lex一个词法分析器的生成器 翻译版.docx
- 文档编号:10909550
- 上传时间:2023-02-23
- 格式:DOCX
- 页数:23
- 大小:35.63KB
lex一个词法分析器的生成器 翻译版.docx
《lex一个词法分析器的生成器 翻译版.docx》由会员分享,可在线阅读,更多相关《lex一个词法分析器的生成器 翻译版.docx(23页珍藏版)》请在冰豆网上搜索。
lex一个词法分析器的生成器翻译版
Lex-词法分析器生成器(M.E.Lesk,E.Schmidt著,中文翻译)
Lex-词法分析器生成器
M.E.Lesk与E.Schmidt
BellLaboratories
MurrayHill,NewJersey07974
翻译:
寒蝉退士
译者声明:
译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:
http:
//cm.bell-
摘要
Lex帮助书写其控制流由在输入流中的正则表达式的实例来导向的程序。
它适合于编辑器脚本类型的变换,和为解析例程做准备工作而分解输入。
Lex源码是正则表达式和相应的程序片段的表格。
Lex把这个表格变换成读取输入流、复制它到输出流、并把输入划分到匹配给定表达式的字符串中的一个程序。
随着每个这种字符串被识别出来,相应的程序片段就被执行。
表达式通过用Lex生成的确定有限自动机来识别。
用户书写的程序片段按照对应的正则表达式在输入流中出现的次序来执行。
用Lex写成的词法分析程序接受有歧义的规定,并在每个输入点上选择最长的匹配可能。
如果需要,在输入上进行实质的超前查看(lookahead),但输入流会被回退(backup)到当前划分的结束处,所以用户有操纵它的普遍自由。
Lex可以生成用C语言或Ratfor语言写的分析器,Ratfor可以自动的转换成可移植的Fortran。
它可以在PDP-11UNIX、HoneywellGCOS和IBMOS系统上得到。
本手册只讨论在UNIX系统上的生成C语言的分析器,这是在UNIX第7版中唯一支持的Lex形式。
设计Lex时简化了与编译器的编译系统Yacc的交接。
July21,1975
目录
∙1. 介绍
∙2. Lex源码
∙3. Lex正则表达式
∙4. Lex动作
∙5. 歧义源规则
∙6. Lex源定义
∙7. 用法
∙8. Lex与Yacc
∙9. 例子
∙10.左上下文敏感
∙11.字符集
∙12.源格式总结
∙13.告诫和缺陷
∙14.致谢
∙15.引用
1.介绍
Lex是设计用于字符输入流的词法处理的一个程序生成器。
它接受高级的、面向问题的对字符串匹配的规定,并生成识别正则表达式的通用语言写的一个程序。
正则表达式在用户给Lex的源规定中指定。
Lex写出的代码识别在输入流中的这些表达式,并把输入流划分到匹配这些表达式的字符串中。
在字符串间的分界上执行用户提供的程序片段。
Lex源文件对正则表达式关联上程序片段。
随着每个表达式出现在给Lex写出的程序的输入中,相应的程序片段就被执行。
用户提供超出表达式所需要的额外代码来完成他的任务,可能包括用其他生成器写出的代码。
生成的识别表达式的程序采用用户的程序片段所采用的通用编程语言。
因此,提供了高级表达式语言来写要被匹配的字符串表达式,而用户写动作的自由不受侵犯。
这避免了强制希望使用字符串操纵语言做输入分析的用户、去使用同样的并且经常不适合字符串处理的语言来书写处理程序。
Lex不是完整的语言,而是体现了可增加到叫做“宿主语言”的不同编程语言中的新语言特征的一个生成器。
正如同通用语言可以生成在不同计算机硬件上运行的代码,Lex可以写出不同宿主语言的代码。
宿主语言被用于Lex生成的输出代码和用户增加的程序片段。
还为不同的宿主语言提供兼容的运行时间库。
这使Lex适应不同的环境和不同的用户。
每个应用都可以被定向到适合这个任务的硬件和宿主语言、用户背景和本地实现性质的各种组合上。
目前唯一支持的宿主语言是C,尽管过去曾经支持过Fortran(Ratfor[2]形式)。
Lex自身存在于UNIX、GCOS和OS/370;但Lex生成的代码可以采用于存在适当的编译器的任何地方。
Lex把用户的表达式和动作(在本文中叫做源码)转换成宿主通用语言;生成的程序叫做yylex。
yylex程序将识别在流(在本文中叫做输入)中的表达式,并在检测到的时候执行给每个表达式的动作。
参见图1。
+-------+
源码->|Lex|->yylex
+-------+
+-------+
输入->|yylex|->输出
+-------+
Lex概述
图1
作为一个平凡的例子,考虑从输入中删除所有行结束处的空白或tab的程序。
%%
[\t]+$;
就是所需要的。
这个程序包含标记规则开始的一个%%分界符和一个规则。
这个规则包含匹配在行结束处之前的空白或tab(按照C语言约定写为\t)字符的一个或多个实例的一个正则表达式。
方括号指示由空白和tab构成的字符类;同于QED,+指示“一或多个”;而$指示“行结束”。
没有指定动作,所以Lex生成的程序(yylex)将忽略这些字符。
所有其他东西都被复制。
要把任何余下的空白或tab的字符串改变为一个单一的空白,可增加另一个规则:
%%
[\t]+$;
[\t]+printf("");
为这个源码生成的有限自动机将立刻扫描这两个规则,在空白或tab的字符串的终止处观察是否有一个换行字符,并执行想要的规则动作。
第一个规则匹配在行结束处的所有空白或tab的字符串,而第二个规则匹配所有余下的空白或tab的字符串。
Lex可以单独的用做简单变换,或在词法层次上聚集(gather)出的分析和统计。
Lex还可以与解析器一起使用而进行词法分析阶段;交接Lex与Yacc[3]是特别容易的。
Lex程序只识别正则表达式;Yacc写出接受一大类上下文无关文法的解析器,但是需要底层的分析器来识别输入记号(token)。
所以,Lex和Yacc的组合经常是适当的。
在用做后面的解析器生成器的预处理器的时候,Lex用来划分输入流,而解析器生成器指派文法结构到结果的词法块(piece)。
这种情况下的(编译器的前半部分)控制流在图2中展示。
其他生成器或手工写的额外的程序可以轻易的增加到Lex写出的程序。
词法规则文法规则
||
vv
+---------++---------+
|Lex||Yacc|
+---------++---------+
||
vv
+---------++---------+
输入->|yylex|->|yyparse|->解析后的输入
+---------++---------+
Lex与Yacc一起
图2
Yacc用户会发现yylex的名字就是Yacc期望词法分析器叫的名字,所以Lex采用这个名字简化了交接。
Lex从源码中的正则表达式生成一个确定有限自动机[4]。
为了节省空间,这个自动机是解释的而不是编译的。
结果仍是一个快速的分析器。
特别是,Lex程序识别和划分输入流所花费的时间正比于输入的长度。
Lex规则的数目或规则的复杂性在决定速度上都不重要,除了包含前向(forward)上下文的规则需要大量的重新扫描之外。
与规则的数目和复杂性一起增加的是自动机的大小,因此也是Lex生成的程序的大小。
在Lex写出的程序中,用户的片段(表示在找到每个正则表达式时要进行的动作)被收集一起成为switch语句中各个case。
自动机的解释器导向控制流。
提供给用户在包含动作的例程中插入声明或增加语句,或者在这些动作例程之外增加例程的机会。
Lex不受限制于可在提前查看一个字符基础上得到解释的源码。
例如,如果有两个规则一个查找ab而另一个查找abcdefg,则在输入流是abcdefh的时候,Lex将识别ab并留下输入指针正好在cd之前。
这种回退比起简单语言的处理要更加高代价。
2.Lex源码
Lex源码的一般格式为:
{定义}
%%
{规则}
%%
{用户子例程}
这里的定义和用户子例程部分经常被省略。
第二个%%是可选的,但是需要第一个来标记规则的开始。
绝对极小的Lex程序是
%%
(没有定义,没有规则)它被变换成无改变的复制输入到输出的一个程序。
在上述Lex程序的轮廓中,规则代表用户的控制决定;它们是个表格,在其中左列包含正则表达式(参见章节3)而右列包含动作,在识别了表达式的时候要执行的程序片段。
所以如下一个单独规则
integerprintf("foundkeywordINT");
在输入流中查找integer字符串并在它出现的时候打印消息“foundkeywordINT”。
在这个例子中宿主过程语言是C并使用C库函数printf打印这个字符串。
表达式的结束通过第一个空白或tab字符来指示。
如果动作只是一个单一的C语言表达式,它可以给出在这行的右侧;如果它是复合的或多余一行,则应当包围在花括号中。
作为稍微有用一些的例子,假设想要把一些美式拼写改为英式拼写。
Lex规则如下
colourprintf("color");
mechaniseprintf("mechanize");
petrolprintf("gas");
将是一个好的开始。
这些规则非常不够,因为单词petroleum将变成gaseum;后面会讨论处理它的方式。
3.Lex正则表达式
正则表达式的定义非常类似于QED[5]中的定义。
正则表达式指定要被匹配的一组字符串。
它包含文本字符(它匹配被比较的字符串中的相应的字符)和操作符字符(它指定重复、选择和其他特征)。
字母表字母和数字总是文本字符;所以正则表达式
integer
匹配字符串integer只要它出现,而表达式
a57D
查找字符串a57D。
操作符
操作符字符有
"\[]^-?
.*+|()$/{}%<>
如果它们被用做文本字符,应当使用转义(escape)。
引号操作符(")指示在一对引号之间包含的任何东西都被当作文本字符。
所以
xyz"++"
匹配字符串xyz++,在它出现的时候。
注意可以引用字符串的一部分。
引用普通字符是无害和没有必要的;表达式
"xyz++"
同于上面的表达式。
所以通过引用用做文本字符的所有非字母数字字符,用户可以避免记住上面的操作字符列表,并在Lex进一步扩展这个列表的时候是安全。
还可以通过前导一个\来把一个操作符字符转义为一个文本字符,比如
xyz\+\+
是上述表达式的另一个更少可读性的等价者。
引用机制的另一个用处是把空白介入到表达式中;如上所述正常情况下空白或tab结束一个规则。
不包含在[](见后)内的任何空白字符必须被引用。
还识别使用\的一些常见的C转义:
\n是换行、\t是tab而\b是退格(backspace)。
要录入\自身需使用\\。
因为换行在一个表达式中是非法的,必须使用\n;不需要转义tab和退格。
除了空白、tab、换行和上述列表中字符之外的所有字符总是文本字符。
字符类
字符类可以使用运算符对[]来指定。
构造[abc]匹配一个单一字符,可以是a、b或c。
在方括号内,忽略多数字符的意义。
只有三个字符特殊:
它们是\-和^。
-字符指示范围。
例如
[a-z0-9<>_]
指示包含所有小写字母、数字、尖括号和下划线的字符类。
可以按任意次序给出范围。
使用不都是大写字母、不都是小写字母、或不都是数字的在任何一对字符之间的“-”都是依赖实现的并会得到一个警告消息。
(比如,[0-z]在ASCII中比在EBCDIC中有更多字符)。
如果需要在字符类中包含字符-,它应当在第一个或最后一个位置上;所以
[-+0-9]
匹配所有数字和两个算符。
在字符类中,^操作符必须出现为左方括号后的第一个字符;它只是结果的字符串关于计算机字符集的补集。
所以
[^abc]
匹配除了a、b或c的所有字符,包括了特殊或控制字符;而
[^a-zA-Z]
是不是字母的任何字符。
\字符提供在字符类方括号内的正常转义。
任意字符
为了匹配最任何的字符,操作符字符.是除了换行之外所有字符的类。
转义八进制数是可能的,尽管是不可移植的:
[\40-\176]
匹配在ASCII字符集中所有可打印字符,从八进制的40(空白)到八进制的176(波浪线)。
可选的表达式
操作符?
指示表达式的一个可选元素。
所以
ab?
c
匹配ac或者abc。
重复的表达式
操作符*和+指示类的重复。
a*
是任何数目的连续的a字符,包括零个;而
a+
是a的一个或多个实例。
例如
[a-z]+
是所有小写字母的字符串。
而
[A-Za-z][A-Za-z0-9]*
指示开始于字母字符的所有字母数字的字符串。
这是识别计算机语言中标识符的典型表达式。
选择和组合
操作符|指示选择:
(ab|cd)
匹配要么ab要么cd。
注意使用圆括号作为组合,尽管它们在最外层不是必须的;
ab|cd
就足够了。
可以使用圆括号形成更复杂的表达式:
(ab|cd+)?
(ef)*
匹配字符串如abefef、efefef、cdef或cddd;但不匹配abc、abcd或abcdef。
上下文敏感
Lex会识别少量的外围上下文。
两个最简单的这种操作符是^和$。
如果一个表达式的第一个字符是^,这个表达式将只在一行的开始处被匹配(在一个换行字符之后,或在输入流的开始处)。
这永不会冲突于^的其他意义,即表示字符类的补集,因为它只适用在[]操作符内。
如果最后的字符是$,则这个表达式只在一行的结束处被匹配(在立即跟随着换行的时候)。
$操作符是指示尾随上下文的/操作符字符的特殊情况。
表达式
ab/cd
匹配字符串ab,但只在它跟随着cd的时候。
所以
ab$
同于
ab/\n
左上下文在Lex中通过在章节10中解说的开始条件来处理。
如果一个规则只在Lex自动机解释器处在开始条件x下的时候执行,这个规则应前导着
使用了尖括号操作符字符。
如果我们把“在一行的开始处”考虑为开始条件ONE,则^操作符将等价于
后面会详细解说开始条件。
重复和定义
操作符{}指定要么重复(如果包围了数字)要么定义展开(如果包围了一个名字),例如
{digit}
查找叫做digit的一个预定义的字符串并把它插入到这个表达式中这一点上。
在Lex输入中这种定义在规则前面的第一部分中给出。
相反的
a{1,5}
查找a的1到5次出现。
最后,初始的%是特殊的,它作为Lex源码分段的分隔符。
4.Lex动作
当按如上规定所写出的表达式被匹配了的时候,Lex就执行相应的动作。
本章节描述Lex辅助书写动作的某些特征。
注意有一个缺省动作把输入复制到输出。
它进行在所有没有匹配的字符串上。
所以希望抛弃整个输入而不产生任何输出的Lex用户必须提供匹配所有东西的规则。
当与Yacc一起使用Lex的时候,这是正常情况。
你可能认为动作就是替代复制输入到输出而所要做的;所以一般而言,只做复制的规则可以省略。
从规则中被忽略了的、并出现在了输入中的字符组合好象应该被打印在输出中,以此引起对规则有缺口的注意。
可以做的最简单的事情是忽略输入。
指定一个C空语句;作为动作导致这种结果。
常见的规则是
[\t\n];
它导致三个间隔字符(空白、tab和换行)被忽略。
避免写动作的另一个容易的方式是动作字符|,它指示给这个动作的规则是给下一个动作的规则。
前面的例子还可以写为
""|
"\t"|
"\n";
有相同的结果却有着不同的风格。
在\n和\t外围的引号是不必须的。
在更加复杂的动作中,用户经常希望知道匹配某个表达式如[a-z]+的实际文本。
Lex把这个文本保留在叫做yytext的外部字符数组中。
所以要打印找到的字符串,使用如下规则
[a-z]+printf("%s",yytext);
将打印在yytext中的字符串。
C函数printf接受一个格式参数和要打印的数据;在这个情况下,格式是“打印字符串”(%指示数据转换,而s指示字符串类型),数据是在yytext中的字符。
所以这正好把匹配的字符串放置到输出中。
这个动作如此的常见以至于可以简写为ECHO:
[a-z]+ECHO;
它和上面的一样。
因为缺省动作就是打印找到的字符串,你可能要问为什么要给出这么一个只是指定缺省动作的规则?
经常需要这种规则来避免匹配不是想要的某些规则。
例如,如果有一个规则匹配read,则它将正常的匹配包含在bread或readjust中的read的实例;为了避免如此,需要形如[a-z]+的一个规则。
这将在下面解说。
有时知道已经找到的东西的结束处是更加方便的;所以Lex还提供匹配的字符数目的计数yyleng。
要计数在输入中的字的数目和在字中字符的数目,用户可以写
[a-zA-Z]+{words++;chars+=yyleng;}
它以字节为单位累计识别了的字中的字符数目。
在匹配的字符串中的最后的字符可以如下这样访问
yytext[yyleng-1]
有时,Lex动作可以确定仍未被识别的规则的正确字符跨度(span)。
提供了两个例程在这种情况下做辅助。
首先,可以调用yymore()来指示下一个识别的输入表达式要续加到这个输入的结束处。
正常的,下一个输入字符串会覆写yytext中的当前内容。
其次,可以调用yyless(n)来指示不是当前成功表达式匹配的所有字符都是现在就需要的。
参数n指示在yytext中要保留的字符的数目。
更多的以前匹配了的字符被返回给输入。
这提供了与/操作符所提供的同类的超前查看,但有不同的形式。
例子:
考虑一种语言,它定义字符串为在引号(")之间的一组字符,并规定在字符串中包括"必须前导上\。
匹配它的正则表达式有些混乱,所以如下这么写会更好
\"[^"]*{
if(yytext[yyleng-1]=='\\')
yymore();
else
...正常用户处理
}
在面对字符串如"abc\"def"的时候,它首先匹配前五个字符"abc\;接着调用yymore()导致字符串的下一部分"def被续加到结束处上。
注意终止这个字符串的最后的引号将被选取到标记为“正常处理”的那部分代码中。
函数yyless()可以用来在各种条件下重处理文本。
考虑老版本C语言的区分“=-a”的歧义的问题。
假设想要把它处理为“=-a”但要打印一个消息。
可用一个规则
=-[a-zA-Z]{
printf("Op(=-)ambiguous\n");
yyless(yyleng-1);
...对=-的动作...
}
它打印一个消息,把在这个操作符之后的字母返回到输入,并把操作符当作“=-”。
作为另一种选择,想要把它处理为“=-a”。
要如此只需要把减号同字母一起返回到输入:
=-[a-zA-Z]{
printf("Op(=-)ambiguous\n");
yyless(yyleng-2);
...对=的动作...
}
将进行此种释义。
注意对这两种情况的表达式可以轻易写为对第一种情况的
=-/[A-Za-z]
和对第二种情况的
=/-[A-Za-z]
;在规则动作中不需要回退。
不需要识别整个标识符来察觉这个歧义。
但是有“=-3”的可能性使
=-/[^\t\n]
是更好的规则。
除了这些例程,Lex还允许访问它所使用的I/O例程。
它们是:
1.input()返回下一个输入字符;
2.output(c)把字符c写到输出;和
3.unput(c)把字符c压回到输入流中以被后来的input()读取。
缺省的把这些例程作为宏定义提供,但用户可以屏弃它们并提供私有版本。
这些例程定义了在外部文件和内部字符之间的关联,因而必须一致的都被保留或都被修改。
它们可以被重定义,来导致同陌生的地方传送输入或输出,包括同其他程序或内部内存;但使用的字符集必须在所有例程中是一致的;从输入返回零值必须意味着文件结束;在unput和input之间的关联必须被保持,否则Lex超前查看将不能工作。
Lex根本不超前查看,如果不是必须的话,但结束于+*?
或$,或包含/的所有规则隐含了超前查看。
超前对于匹配是另一个表达式的前缀的表达式也是必须的。
后面有对Lex使用的字符集的讨论。
标准Lex库在回退上施加了100个字符的限制。
另一个用户可能想重定义的Lex库例程是yywrap(),Lex在到达文件结束的时候调用它。
如果yywrap返回1,Lex继续在输入结束时的正常包装(wrapup)。
但是,有时重新安排更多的来自新来源的输入是方便的。
在这种情况下,用户应当提供一个重新安排新输入并返回0的yywrap。
这指示Lex继续处理。
缺省的yywrap总是返回1。
这个例程还是在程序结束的时候打印表格和总结的方便地方。
注意写一个识别文件结束的正常规则是不可能的;对这个条件的唯一访问就是通过yywrap。
事实上,除非提供一个私有版本的input(),包含空字符(\0)文件是不能被处理的,因为从输入返回的0值被当作文件结束。
5.歧义源规则
Lex可以处理有歧义的规定。
当多于一个表达式可以匹配当前输入的时候,Lex按如下来选择:
1.首选最长匹配。
2.在匹配相同数目字符的规则中,首选最先给出的规则。
所以假定规则
integer关键字动作...;
[a-z]+标识符动作...;
按这个次序给出。
如果输入是integers,它被接受为标识符,因为[a-z]+匹配8个字符而integer只匹配7个。
如果输入是integer,两个规则都匹配7个字符,选择关键字规则因为它是第一个。
任何更短的(比如int)将不匹配表达式integer所以使用标识符释义。
首选最长匹配的原理使包含.*表达式的规则是危险的。
例如'.*'好象是识别在单引号中的字符串的好方式。
但是它诱使程序更加超前的读取,查找最远的单引号。
假设输入是
'first'quotedstringhere,'second'here
上述表达式将匹配
'first'quotedstringhere,'second'
这不大可能是想要的。
更好的规则有如下形式
'[^'\n]*'
它在上述输入上将停止在'first'之后。
.操作符不匹配换行的事实减轻了这种错误结果。
所以表达式如.*停止于当前行之上。
不要尝试通过表达式如(.|\n)+或等价者来克服这个限制;Lex生成的程序将尝试读取整个输入文件,导致内部缓冲区溢出。
注意Lex正常的划分输入流,不查找每
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- lex一个词法分析器的生成器 翻译版 lex 一个 词法 分析器 生成器 翻译