Yacc+与+Lex+快速入门.docx

上传人:b****8 文档编号:12795592 上传时间:2023-06-08 格式:DOCX 页数:16 大小:24.19KB
下载 相关 举报
Yacc+与+Lex+快速入门.docx_第1页
第1页 / 共16页
Yacc+与+Lex+快速入门.docx_第2页
第2页 / 共16页
Yacc+与+Lex+快速入门.docx_第3页
第3页 / 共16页
Yacc+与+Lex+快速入门.docx_第4页
第4页 / 共16页
Yacc+与+Lex+快速入门.docx_第5页
第5页 / 共16页
Yacc+与+Lex+快速入门.docx_第6页
第6页 / 共16页
Yacc+与+Lex+快速入门.docx_第7页
第7页 / 共16页
Yacc+与+Lex+快速入门.docx_第8页
第8页 / 共16页
Yacc+与+Lex+快速入门.docx_第9页
第9页 / 共16页
Yacc+与+Lex+快速入门.docx_第10页
第10页 / 共16页
Yacc+与+Lex+快速入门.docx_第11页
第11页 / 共16页
Yacc+与+Lex+快速入门.docx_第12页
第12页 / 共16页
Yacc+与+Lex+快速入门.docx_第13页
第13页 / 共16页
Yacc+与+Lex+快速入门.docx_第14页
第14页 / 共16页
Yacc+与+Lex+快速入门.docx_第15页
第15页 / 共16页
Yacc+与+Lex+快速入门.docx_第16页
第16页 / 共16页
亲,该文档总共16页,全部预览完了,如果喜欢就下载吧!
下载资源
资源描述

Yacc+与+Lex+快速入门.docx

《Yacc+与+Lex+快速入门.docx》由会员分享,可在线阅读,更多相关《Yacc+与+Lex+快速入门.docx(16页珍藏版)》请在冰点文库上搜索。

Yacc+与+Lex+快速入门.docx

Yacc+与+Lex+快速入门

Yacc与Lex快速入门

Lex与Yacc介绍

AshishBansal

软件工程师,Sapient公司

2000年11月

Lex和Yacc是UNIX的两种非常重要的、功能强大的工具。

事实上,如果你熟练掌握Lex和Yacc的话,它们的强大功能使创建FORTRAN和C的编译器如同儿戏。

AshishBansal为您详细的讨论了编写自己的语言和编译器所用到的这两种工具,包括常规表达式、声明、匹配模式、变量、Yacc语法和解析器代码。

最后,他解释了怎样将Lex和Yacc结合起来。

Lex代表LexicalAnalyzar。

Yacc代表YetAnotherCompilerCompiler。

让我们从Lex开始吧。

Lex

Lex是一种生成扫描器的工具。

扫描器是一种识别文本中的词汇模式的程序。

这些词汇模式(或者常规表达式)在一种特殊的句子结构中定义,这个我们一会儿就要讨论。

一种匹配的常规表达式可能会包含相关的动作。

这一动作可能还包括返回一个标记。

当Lex接收到文件或文本形式的输入时,它试图将文本与常规表达式进行匹配。

它一次读入一个输入字符,直到找到一个匹配的模式。

如果能够找到一个匹配的模式,Lex就执行相关的动作(可能包括返回一个标记)。

另一方面,如果没有可以匹配的常规表达式,将会停止进一步的处理,Lex将显示一个错误消息。

Lex和C是强耦合的。

一个.lex文件(Lex文件具有.lex的扩展名)通过lex公用程序来传递,并生成C的输出文件。

这些文件被编译为词法分析器的可执行版本。

Lex的常规表达式

常规表达式是一种使用元语言的模式描述。

表达式由符号组成。

符号一般是字符和数字,但是Lex中还有一些具有特殊含义的其他标记。

下面两个表格定义了Lex中使用的一些标记并给出了几个典型的例子。

用Lex定义常规表达式

字符

含义

A-Z,0-9,a-z

构成了部分模式的字符和数字。

.

匹配任意字符,除了\n。

-

用来指定范围。

例如:

A-Z指从A到Z之间的所有字符。

[]

一个字符集合。

匹配括号内的任意字符。

如果第一个字符是^那么它表示否定模式。

例如:

[abC]匹配a,b,和C中的任何一个。

*

匹配0个或者多个上述的模式。

+

匹配1个或者多个上述模式。

?

匹配0个或1个上述模式。

$

作为模式的最后一个字符匹配一行的结尾。

{}

指出一个模式可能出现的次数。

例如:

A{1,3}表示A可能出现1次或3次。

\

用来转义元字符。

同样用来覆盖字符在此表中定义的特殊意义,只取字符的本意。

^

否定。

|

表达式间的逻辑或。

"<一些符号>"

字符的字面含义。

元字符具有。

/

向前匹配。

如果在匹配的模版中的“/”后跟有后续表达式,只匹配模版中“/”前面的部分。

如:

如果输入A01,那么在模版A0/1中的A0是匹配的。

()

将一系列常规表达式分组。

常规表达式举例

常规表达式

含义

joke[rs]

匹配jokes或joker。

A{1,2}shis+

匹配AAshis,Ashis,AAshi,Ashi。

(A[b-e])+

匹配在A出现位置后跟随的从b到e的所有字符中的0个或1个。

Lex中的标记声明类似C中的变量名。

每个标记都有一个相关的表达式。

(下表中给出了标记和表达式的例子。

)使用这个表中的例子,我们就可以编一个字数统计的程序了。

我们的第一个任务就是说明如何声明标记。

标记声明举例

标记

相关表达式

含义

数字(number)

([0-9])+

1个或多个数字

字符(chars)

[A-Za-z]

任意字符

空格(blank)

""

一个空格

字(word)

(chars)+

1个或多个chars

变量(variable)

(字符)+(数字)*(字符)*(数字)*

Lex编程

Lex编程可以分为三步:

1.以Lex可以理解的格式指定模式相关的动作。

2.在这一文件上运行Lex,生成扫描器的C代码。

3.编译和链接C代码,生成可执行的扫描器。

注意:

如果扫描器是用Yacc开发的解析器的一部分,只需要进行第一步和第二步。

关于这一特殊问题的帮助请阅读Yacc和将Lex和Yacc结合起来部分。

现在让我们来看一看Lex可以理解的程序格式。

一个Lex程序分为三个段:

第一段是C和Lex的全局声明,第二段包括模式(C代码),第三段是补充的C函数。

例如,第三段中一般都有main()函数。

这些段以%%来分界。

那么,回到字数统计的Lex程序,让我们看一下程序不同段的构成。

C和Lex的全局声明

这一段中我们可以增加C变量声明。

这里我们将为字数统计程序声明一个整型变量,来保存程序统计出来的字数。

我们还将进行Lex的标记声明。

字数统计程序的声明

%{

intwordCount=0;

%}

chars[A-za-z\抃'\.\"]

numbers([0-9])+

delim[""\n\t]

whitespace{delim}+

words{chars}+

%%

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

Lex的模式匹配规则

让我们看一下Lex描述我们所要匹配的标记的规则。

(我们将使用C来定义标记匹配后的动作。

)继续看我们的字数统计程序,下面是标记匹配的规则。

字数统计程序中的Lex规则

{words}{wordCount++;/*

increasethewordcountbyone*/}

{whitespace}{/*do

nothing*/}

{numbers}{/*onemay

wanttoaddsomeprocessinghere*/}

%%

C代码

Lex编程的第三段,也就是最后一段覆盖了C的函数声明(有时是主函数)。

注意这一段必须包括yywrap()函数。

Lex有一套可供使用的函数和变量。

其中之一就是yywrap。

一般来说,yywrap()的定义如下例。

我们将在高级Lex中探讨这一问题。

字数统计程序的C代码段

voidmain()

{

yylex();/*startthe

analysis*/

printf("Noofwords:

%d\n",wordCount);

}

intyywrap()

{

return1;

}

上一节我们讨论了Lex编程的基本元素,它将帮助你编写简单的词法分析程序。

在高级Lex这一节中我们将讨论Lex提供的函数,这样你就能编写更加复杂的程序了。

将它们全部结合起来

.lex文件是Lex的扫描器。

它在Lex程序中如下表示:

$lex

这生成了lex.yy.c文件,它可以用C编译器来进行编译。

它还可以用解析器来生成可执行程序,或者在链接步骤中通过选项杔l包含Lex库。

这里是一些Lex的标志:

∙-c表示C动作,它是缺省的。

∙-t写入lex.yy.c程序来代替标准输出。

∙-v提供一个两行的统计汇总。

∙-n不打印-v的汇总。

高级Lex

Lex有几个函数和变量提供了不同的信息,可以用来编译实现复杂函数的程序。

下表中列出了一些变量和函数,以及它们的使用。

详尽的列表请参考Lex或Flex手册(见后文的资源)。

Lex变量

yyin

FILE*类型。

它指向lexer正在解析的当前文件。

yyout

FILE*类型。

它指向记录lexer输出的位置。

缺省情况下,yyin和yyout都指向标准输入和输出。

yytext

匹配模式的文本存储在这一变量中(char*)。

yyleng

给出匹配模式的长度。

yylineno

提供当前的行数信息。

(lexer不一定支持。

Lex函数

yylex()

这一函数开始分析。

它由Lex自动生成。

yywrap()

这一函数在文件(或输入)的末尾调用。

如果函数的返回值是1,就停止解析。

因此它可以用来解析多个文件。

代码可以写在第三段,这就能够解析多个文件。

方法是使用yyin文件指针(见上表)指向不同的文件,直到所有的文件都被解析。

最后,yywrap()可以返回1来表示解析的结束。

yyless(intn)

这一函数可以用来送回除了前憂?

个字符外的所有读出标记。

yymore()

这一函数告诉Lexer将下一个标记附加到当前标记后。

对Lex的讨论就到这里。

下面我们来讨论Yacc...

Yacc

Yacc代表YetAnotherCompilerCompiler。

Yacc的GNU版叫做Bison。

它是一种工具,将任何一种编程语言的所有语法翻译成针对此种语言的Yacc语法解析器。

它用巴科斯范式(BNF,BackusNaurForm)来书写。

按照惯例,Yacc文件有.y后缀。

编译行如下调用Yacc编译器:

$yacc

在进一步阐述以前,让我们复习一下什么是语法。

在上一节中,我们看到Lex从输入序列中识别标记。

如果你在查看标记序列,你可能想在这一序列出现时执行某一动作。

这种情况下有效序列的规范称为语法。

Yacc语法文件包括这一语法规范。

它还包含了序列匹配时你想要做的事。

为了更加说清这一概念,让我们以英语为例。

这一套标记可能是:

名词,动词,形容词等等。

为了使用这些标记造一个语法正确的句子,你的结构必须符合一定的规则。

一个简单的句子可能是名词+动词或者名词+动词+名词。

(如Icare.Seespotrun.)

所以在我们这里,标记本身来自语言(Lex),并且标记序列允许用Yacc来指定这些标记(标记序列也叫语法)。

终端和非终端符号

终端符号:

代表一类在语法结构上等效的标记。

终端符号有三种类型:

命名标记:

这些由%token标识符来定义。

按照惯例,它们都是大写。

字符标记:

字符常量的写法与C相同。

例如,?

?

就是一个字符标记。

字符串标记:

写法与C的字符串常量相同。

例如,"<<"就是一个字符串标记。

lexer返回命名标记。

非终端符号:

是一组非终端符号和终端符号组成的符号。

按照惯例,它们都是小写。

在例子中,file是一个非终端标记而NAME是一个终端标记。

用Yacc来创建一个编译器包括四个步骤:

1.通过在语法文件上运行Yacc生成一个解析器。

2.说明语法:

3. 

o编写一个.y的语法文件(同时说明C在这里要进行的动作)。

o编写一个词法分析器来处理输入并将标记传递给解析器。

这可以使用Lex来完成。

o编写一个函数,通过调用yyparse()来开始解析。

o编写错误处理例程(如yyerror())。

4.编译Yacc生成的代码以及其他相关的源文件。

5.将目标文件链接到适当的可执行解析器库。

用Yacc编写语法

如同Lex一样,一个Yacc程序也用双百分号分为三段。

它们是:

声明、语法规则和C代码。

我们将解析一个格式为姓名=年龄的文件作为例子,来说明语法规则。

我们假设文件有多个姓名和年龄,它们以空格分隔。

在看Yacc程序的每一段时,我们将为我们的例子编写一个语法文件。

C与Yacc的声明

C声明可能会定义动作中使用的类型和变量,以及宏。

还可以包含头文件。

每个Yacc声明段声明了终端符号和非终端符号(标记)的名称,还可能描述操作符优先级和针对不同符号的数据类型。

lexer(Lex)一般返回这些标记。

所有这些标记都必须在Yacc声明中进行说明。

在文件解析的例子中我们感兴趣的是这些标记:

name,equalsign,和age。

Name是一个完全由字符组成的值。

Age是数字。

于是声明段就会像这样:

文件解析例子的声明

%

#typedefchar*string;/*

tospecifytokentypesaschar**/

#defineYYSTYPEstring/*

aYaccvariablewhichhasthevalueofreturnedtoken*/

%}

%tokenNAMEEQAGE

%%

你可能会觉得YYSTYPE有点奇怪。

但是类似Lex,Yacc也有一套变量和函数可供用户来进行功能扩展。

YYSTYPE定义了用来将值从lexer拷贝到解析器或者Yacc的yylval(另一个Yacc变量)的类型。

默认的类型是int。

由于字符串可以从lexer拷贝,类型被重定义为char*。

关于Yacc变量的详细讨论,请参考Yacc手册(见资源)。

Yacc语法规则

Yacc语法规则具有以下一般格式:

result:

components{/*

actiontobetakeninC*/}

;

在这个例子中,result是规则描述的非终端符号。

Components是根据规则放在一起的不同的终端和非终端符号。

如果匹配特定序列的话Components后面可以跟随要执行的动作。

考虑如下的例子:

param:

NAMEEQNAME{

printf("\tName:

%s\tValue(name):

%s\n",$1,$3);}

|NAMEEQVALUE{

printf("\tName:

%s\tValue(value):

%s\n",$1,$3);}

;

如果上例中序列NAMEEQNAME被匹配,将执行相应的{}括号中的动作。

这里另一个有用的就是$1和$3的使用,它们引用了标记NAME和NAME(或者第二行的VALUE)的值。

lexer通过Yacc的变量yylval返回这些值。

标记NAME的Lex代码是这样的:

char[A-Za-z]

name{char}+

%%

{name}{yylval=strdup(yytext);

returnNAME;}

文件解析例子的规则段是这样的:

文件解析的语法

file:

recordfile

|record

;

record:

NAMEEQAGE{

printf("%sisnow%syearsold!

!

!

",$1,$3);}

;

%%

附加C代码

现在让我们看一下语法文件的最后一段,附加C代码。

(这一段是可选的,如果有人想要略过它的话:

)一个函数如main()调用yyparse()函数(Yacc中Lex的yylex()等效函数)。

一般来说,Yacc最好提供yyerror(charmsg)函数的代码。

当解析器遇到错误时调用yyerror(charmsg)。

错误消息作为参数来传递。

一个简单的yyerror(char*)可能是这样的:

intyyerror(char*msg)

{

printf("Error:

%s

encounteredatlinenumber:

%d\n",msg,yylineno);

}

yylineno提供了行数信息。

这一段还包括文件解析例子的主函数:

附加C代码

voidmain()

{

yyparse();

}

intyyerror(char*msg)

{

printf("Error:

%s

encountered\n",msg);

要生成代码,可能用到以下命令:

$yacc杁

这生成了输出文件y.tab.h和y.tab.c,它们可以用UNIX上的任何标准C编译器来编译(如gcc)。

命令行的其他常用选项

∙'-d','--defines':

编写额外的输出文件,它们包含这些宏定义:

语法中定义的标记类型名称,语义的取值类型YYSTYPE,以及一些外部变量声明。

如果解析器输出文件名叫'name.c',那么'-d'文件就叫做'name.h'。

如果你想将yylex定义放到独立的源文件中,你需要'name.h',因为yylex必须能够引用标记类型代码和yylval变量。

∙'-bfile-prefix','--file-prefix=prefix':

指定一个所有Yacc输出文件名都可以使用的前缀。

选择一个名字,就如输入文件名叫'prefix.c'.

∙'-ooutfile','--output-file=outfile':

指定解析器文件的输出文件名。

其他输出文件根据'-d'选项描述的输出文件来命名。

Yacc库通常在编译步骤中自动被包括。

但是它也能被显式的包括,以便在编译步骤中指定杔y选项。

这种情况下的编译命令行是:

$cc

names>-ly

将Lex与Yacc结合起来

到目前为止我们已经分别讨论了Lex和Yacc。

现在让我们来看一下他们是怎样结合使用的。

一个程序通常在每次返回一个标记时都要调用yylex()函数。

只有在文件结束或者出现错误标记时才会终止。

一个由Yacc生成的解析器调用yylex()函数来获得标记。

yylex()可以由Lex来生成或完全由自己来编写。

对于由Lex生成的lexer来说,要和Yacc结合使用,每当Lex中匹配一个模式时都必须返回一个标记。

因此Lex中匹配模式时的动作一般格式为:

{pattern}{/*dosmthg*/

returnTOKEN_NAME;}

于是Yacc就会获得返回的标记。

当Yacc编译一个带有杁标记的.y文件时,会生成一个头文件,它对每个标记都有#define的定义。

如果Lex和Yacc一起使用的话,头文件必须在相应的Lex文件.lex中的C声明段中包括。

让我们回到名字和年龄的文件解析例子中,看一看Lex和Yacc文件的代码。

Name.y-语法文件

%

typedefchar*string;

#defineYYSTYPEstring

%}

%tokenNAMEEQAGE

%%

file:

recordfile

|record

;

record:

NAMEEQAGE{

printf("%sis%syearsold!

!

!

\n",$1,$3);}

;

%%

intmain()

{

yyparse();

return0;

}

intyyerror(char*msg)

{

printf("Error

encountered:

%s\n",msg);

}

Name.lex-Lex的解析器文件

%{

#include"y.tab.h"

#include

#include

externchar*yylval;

%}

char[A-Za-z]

num[0-9]

eq[=]

name{char}+

age{num}+

%%

{name}{yylval=strdup(yytext);

returnNAME;}

{eq}{returnEQ;}

{age}{yylval=strdup(yytext);

returnAGE;}

%%

intyywrap()

{

return1;

}

作为一个参考,我们列出了y.tab.h,Yacc生成的头文件。

y.tab.h-Yacc生成的头文件

#defineNAME257

#defineEQ258

#defineAGE259

我们对于Lex和Yacc的讨论到此为止。

今天你想要编译什么语言呢?

资源

∙LexandYacc,Levine,Mason和Branson,O扲eilly及其合作公司,2ndEd。

∙ProgramDevelopmentinUNIX,J.T.Shen,Prentice-HallIndia。

∙Compilers:

Principles,TechniquesandTools,Ahoo,Sethi和Ullman,Addison-WesleyPub.Co.,1985,11。

∙BisonManualfromGnu.

∙FlexManualfromGnu.

∙InfoonBison,(GNU的Yacc).

∙MKSLexandYacc版本注释。

∙LexandYacc指导。

∙LexandYaccandcompilerwriting指导。

∙Java版的Lex指导,叫做Jlex。

∙使用Lex和Yacc的formalizingagrammar实例。

作者介绍

AshishBansal具有印度瓦腊纳西BanarasHindu大学技术学院的电子与通信工程学士学位。

他目前是Sapient公司的软件工程师。

他的Email是mailto:

abansal@ieee.org。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 表格模板 > 调查报告

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2