410 likes | 589 Views
编译原理上机实习. dws@pku.edu.cn. 实习题目. 利用词法分析程序的自动构造工具 LEX 构造一个识别 C 语言的 单词的词法分析程序; 利用语法分析程序的自动构造工具 Yacc 构造一个识别 C 语言的 程序的语法分析程序. LEX 简介 YACC 简介 实习要求. LEX 简介. Lex 是一种生成扫描器的工具。扫描器是一种识别文本中的词汇模式的程序。这些词汇模式(或者常规表达式)在一种特殊的句子结构中定义.
E N D
编译原理上机实习 dws@pku.edu.cn
实习题目 • 利用词法分析程序的自动构造工具LEX构造一个识别C语言的单词的词法分析程序; • 利用语法分析程序的自动构造工具Yacc构造一个识别C语言的程序的语法分析程序
LEX简介 • YACC简介 • 实习要求
LEX简介 • 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编程可以分为三步: 以Lex可以理解的格式指定模式相关的动作。在这一文件上运行Lex,生成扫描器的C代码。编译和链接C代码,生成可执行的扫描器。
一个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规则 • {words}{wordCount++;/* increasethewordcountbyone*/} {whitespace}{/*do nothing*/} {numbers}{/*onemay wanttoaddsomeprocessinghere*/} %%
C代码 • Lex编程的第三段,也就是最后一段覆盖了C的函数声明(有时是主函数)。注意这一段必须包括yywrap()函数。Lex有一套可供使用的函数和变量。其中之一就是yywrap。一般来说,yywrap()的定义如下例。
字数统计程序的C代码段 • voidmain() { yylex();/*startthe analysis*/ printf("Noofwords: %d\n",wordCount); } intyywrap() { return1; }
高级Lex • Lex变量 yyinFILE*类型。它指向lexer正在解析的当前文件。yyoutFILE*类型。它指向记录lexer输出的位置。缺省情况下,yyin和yyout都指向标准输入和输出。yytext匹配模式的文本存储在这一变量中(char*)。yyleng给出匹配模式的长度。yylineno提供当前的行数信息。(lexer不一定支持。)
高级Lex ---Lex函数 • yylex()这一函数开始分析。它由Lex自动生成。yywrap()这一函数在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。因此它可以用来解析多个文件。代码可以写在第三段,这就能够解析多个文件。方法是使用yyin文件指针(见上表)指向不同的文件,直到所有的文件都被解析。最后,yywrap()可以返回1来表示解析的结束。yyless(intn)这一函数可以用来送回除了前憂?个字符外的所有读出标记。yymore()这一函数告诉Lexer将下一个标记附加到当前标记后。
YACC语言简介 • 什么是语法 : • 我们看到Lex从输入序列中识别标记。如果你在查看标记序列,你可能想在这一序列出现时执行某一动作。这种情况下有效序列的规范称为语法。Yacc语法文件包括这一语法规范。它还包含了序列匹配时你想要做的事。
以英语为例,这一套标记可能是:名词,动词,形容词等等。为了使用这些标记造一个语法正确的句子,你的结构必须符合一定的规则。一个简单的句子可能是名词+动词或者名词+动词+名词。以英语为例,这一套标记可能是:名词,动词,形容词等等。为了使用这些标记造一个语法正确的句子,你的结构必须符合一定的规则。一个简单的句子可能是名词+动词或者名词+动词+名词。 • 所以在我们这里,标记本身来自语言(Lex),并且标记序列允许用Yacc来指定这些标记(标记序列也叫语法)。
终端和非终端符号 • 终端符号:代表一类在语法结构上等效的标记。 • 终端符号有三种类型: 命名标记:这些由%token标识符来定义。按照惯例,它们都是大写。 字符标记:字符常量的写法与C相同。例如,??就是一个字符标记。 字符串标记:写法与C的字符串常量相同。例如,"<<"就是一个字符串标记。 lexer返回命名标记。
非终端符号:是一组非终端符号和终端符号组成的符号。按照惯例,它们都是小写。在例子中,file是一个非终端标记而NAME是一个终端标记。
用Yacc来创建一个编译器包括四个步骤: • 编写一个.y的语法文件(同时说明C在这里要进行的动作)。编写一个词法分析器来处理输入并将标记传递给解析器。这可以使用Lex来完成。编写一个函数,通过调用yyparse()来开始解析。编写错误处理例程(如yyerror())。编译Yacc生成的代码以及其他相关的源文件。
用Yacc编写语法 • 如同Lex一样,一个Yacc程序也用双百分号分为三段。它们是:声明、语法规则和C代码。我们将解析一个格式为姓名=年龄的文件作为例子,来说明语法规则。我们假设文件有多个姓名和年龄,它们以空格分隔。在看Yacc程序的每一段时,我们将为我们的例子编写一个语法文件。
C与Yacc的声明 • C声明可能会定义动作中使用的类型和变量,以及宏。还可以包含头文件。每个Yacc声明段声明了终端符号和非终端符号(标记)的名称,还可能描述操作符优先级和针对不同符号的数据类型。lexer(Lex)一般返回这些标记。所有这些标记都必须在Yacc声明中进行说明。
在文件解析的例子中我们感兴趣的是这些标记:name,equalsign,和age。Name是一个完全由字符组成的值。Age是数字。于是声明段就会像这样: 文件解析例子的声明 % #typedefchar*string;/* tospecifytokentypesaschar**/ #defineYYSTYPEstring/* aYaccvariablewhichhasthevalueofreturnedtoken*/ %} %tokenNAMEEQAGE %%
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)的值。
标记NAME的Lex代码是这样的 • char[A-Za-z] name{char}+ %% {name}{yylval=strdup(yytext); returnNAME;}
文件解析例子的规则段是这样的: • 文件解析的语法 file:recordfile |record ; record:NAMEEQAGE{ printf("%sisnow%syearsold!!!",$1,$3);} ; %%
附加C代码 • 一个函数如main()调用yyparse()函数(Yacc中Lex的yylex()等效函数)。一般来说,Yacc最好提供yyerror(charmsg)函数的代码。当解析器遇到错误时调用yyerror(charmsg)。错误消息作为参数来传递。
一个简单的yyerror(char*)可能是这样的: intyyerror(char*msg) { printf("Error:%s encounteredatlinenumber:%d\n",msg,yylineno); }
这一段还包括文件解析例子的主函数: 附加C代码 voidmain() { yyparse(); } intyyerror(char*msg) { printf("Error:%s encountered\n",msg);
实习要求 • 用LEX编写C语言词法分析器 • 输入:C语言程序 输出:词法分析结果 • 用YACC编写C语言语法分析器 • 提交:.c文件和.exe文件 • 提交时间:最晚考试前提交 • ftp上有Pascal的例子
需要的软件 • Parser Generator • 在ftp上包括这个软件的使用方法和安装文件 • VC6或7
C语言 auto break case char const continue default dodoubleelseenumexternfloatforgotoifintlongregisterreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile