首页  编辑  

从lex&yacc说到编译器

Tags: /超级猛料/Alogrith.算法和数据结构/   Date Created:

从lex&yacc说到编译器(1.正则表达式)

作者:tangl_99

QQ:8664220

msn:tangl_99@hotmail.com

email:tangl_99@sohu.com

学过编译原理的朋友肯定都接触过LEX这个小型的词法扫描工具. 但是却很少有人真正把LEX用在自己的程序里. 在构造专业的编译器的时候,常常需要使用到lex和yacc. 正是因为这两个工具,使得我们编写编译器,解释器等工具的时候工作变得非常简单.不过话说回来,会使用lex和yacc的人也确实不简单. Lex和yacc里面牵涉到一系列的编译原理的理论知识,不是简单地看看书就能搞懂的. 本文只是简单地介绍一下lex和yacc的使用方法.相关编译理请查看本科教材.

国内大学教材里面对于lex和yacc的介绍很少,有些根本就没有,不过在国外的编译原理教材介绍了很多. 按照学科的分类,国内大学本科里面开的<<编译原理>>教程只是讲解编译的原理,并不讲解实践. 而对于实践方面则是另外一门学科<<编译技术>>. 关于编译技术的书籍在国内是少之又少. 前不久, 听说上海交大的计科内部出版过编译技术的教材.可惜我们这些人就无法得见了. 还好,机械工业出版社引进了美国 Kenneth C.Louden所著的经典著作<<编译原理及实践>>中,比较详细地介绍lex和yacc的使用.

Lex属于GNU内部的工具,它通常都是gcc的附带工具. 如果你使用的Linux操作系统,那么肯定系统本身就有lex和yacc,不过yacc的名字变成了bison. 如果你使用的Windows操作系统,那么可以到cygwin或者GNUPro里面找得到. 网上也有windows版本lex和yacc,大家可以自己去找一找.

本文一共有两篇,一篇是介绍lex,另一篇是介绍yacc.  Lex和yacc搭配使用, 我们构造自己的编译器或者解释器就如同儿戏. 所以我把本文的名字叫做黄金组合.

本文以flex( Fase Lex)为例,两讲解如何构造扫描程序.

Flex可以通过一个输入文件,然后生成扫描器的C源代码.

其实扫描程序并不只用于编译器 .比如编写游戏的脚本引擎的时候,我看到很多开发者都是自己写的扫描器,其算法相当落后(完全没有DFA的概念化), 甚至很多脚本引擎开发者的词法扫描器都没有编写,而是在运行过程中寻找token(单词). 在现代的计算机速度确实可以上小型的脚本引擎在运行中进行词法扫描, 但是作为一个合格的程序员, 或者说一个合格的计算机本科毕业生而来说, 能够运用编译原理与技术实践,应该是个基本要求.

如果要说到词法分析的扫描器源代码编写, 其实也很简单, 会C语言的人都会写. 可是Kenneth Louden在<<编译原理及技术>里面,花了50多页,原因就是从理论角度,介绍标准的,可扩展的,高效的词法扫描器的编写. 里面从正则表达式介绍到DFA(有穷自动机),再到NFA(非确定性有穷自动机),最后才到代码的编写. 以自动机原理编译扫描器的方法基本上就是现在词法扫描器的标准方法, 也就是Lex使用的方法. 在Lex中,我们甚至不需要自己构造词法的DFA, 我们只需要把相应的正则表达式输入, 然后lex能够为我们自己生成DFA,然后生成源代码,可谓方便之极.

本文不讲DFA, lex的输入是正则表达式, 我们直接先看看正则表达式方面知识就可以了.

1.正则表达式(regular expression):

对于学过编译原理的朋友来说,这一节完全可以不看.不过有些东西还是得注意一下,因为在flex中的正则表达式的使用有些具体的问题是在我们的课本上没有说明的.

先看看例子:

例1.1

name      Tangl_99

这就是定义了name这个正则表达式,它就等于字符串Tangl_99.所以,如果你的源程序中出现了Tangl_99这个字符传,那么它就等于出现一次name正则表达式.

例1.2

digit        0|1|2|3|4|5|6|7|8|9

这个表达式就是说,正则表达式digit就是0,1,2,…,9中的某一个字母.所以无论是0,2,或者是9…都是属于digit这个正则表达式的.

"|"符号表示"或者"的意思.

那么定义正则表达式 name   Tangl_99|Running,同样的,如果你的源程序中出现了Tangl_99或者Running,那么就等于出现了一次name正则表达式.

例1.3

one       1*

"*"符号表示"零到无限次重复"

那么one所表示的字符串就可以是空串(什么字符都没有), 1, 11, 111, 11111, 11111111111, 11111111…等等.总之,one就是由0个或者N个1所组成(N可以为任意自然数).

与"*"相同的有个"+"符号.请看下面的例子1.4

例1.4

realone    1+

"+"符号表示"1到无限次重复"

那么realone和one不同的唯一一点就是,realone不包含空串,因为"+"表示至少一次重复,那么realone至少有一个1.所以realone所表达的字符串就是1,11,111, 1111, 11111…,等等.

例1.5

digit      [0-9]

letter     [a-zA-Z]

这里的digit等于例1.2中的digit,也就是说,a|b|c就相当于[a-c].

同理,letter也就是相当于 a|b|c|d|e|f|…|y|z|A|B|C|D…|Z 不过注意的一点就是,你不能把letter写成[A-z],而必须大写和小写都应该各自写出来.

例1.6

notA        [^A]

"^"表示非,也就是除了这个字符以外的所有字符

所以notA表示的就是除了A以外的所有字符.

下面让我们来看看一些一般高级程序语言中常用的综合例子.

digit        [0-9]

number     {digit}+

letter       [a-zA-Z_]

digit前面多次提起过,就是0-9的阿拉伯数字.number就是所有的数字组合,也就是整数.

Letter前面也提起过,唯一不同的就是多了一个下划线.因为一般我们的C语言中容许有下划线来表示定义的变量名,所以我也把下划线当成英语字母来处理了.

这里number中使用上面定义的digit正则表达式.在lex中,用{digit}就是表示正则表达式digit.

newline      [\n]

whitespace   [ \t]+

newline就是提行的意思.这里我们使用的是\n这个符号,它和C语言中表示提行号一致.问题是大家可能要问到为什么要使用[]符号.因为在lex中,如果你使用[],那么里面表示的肯定就是单个字符号,而不会被理解成"\"和"n"两个字符.

Whitespace就是空格符号的意思.一般的高级程序语言中有两种,一种就是简单的空格,还有一种就是\t制表符.使用了"+"符号,就表示了这些空白符号的无限组合.

---------------------------------------

从lex&yacc说到编译器(2.flex的使用)

作者:tangl_99

QQ:8664220

msn:tangl_99@hotmail.com

email:tangl_99@sohu.com

看了第一篇的关于正则表达式的说明后,下面我们就来通过它,使用flex这个词法分析工具来构造我们的编译器的词法分析器.

关于lex的教程应该是很多,这里我就简单地介绍一下,然后着重后面的lex和yacc的配合使用以及其技巧.所以,如果你不看了后还是不太明白lex或者yacc的使用,请你自己上网去查查,这方面的教程是很多的.我知道的一篇常见的就是

Yacc 与 Lex 快速入门

Lex 与 Yacc 介绍

它的作者就是Ashish Bansal.

Flex就是fast lex的意思.而lex就是Lexical Analyzar的意思.flex可以在cygwin或者gnupro中找到.它是unix的一个工具,属于GNU组织产品.网上也可以找到单独可以在windows下用的版本.

我们一般把我们的词法扫描程序要扫描的一些单词(token)用正则表达式写好,然后作为lex的输入文件,输入命令flex xxx.l(xxx.l就是输入文件),lex经过处理后,就能得到一个名字叫lex.yy.c的C源代码.这个C源代码文件,就是我们的词法扫描程序.通常lex为我们生成的词法分析器的C源代码都是十分复杂而且庞大的,我们一般根本不会去查看里面的代码(放心好了,flex这个东西不会出错的)

下面让我们看看几个我已经使用过的几个lex输入文件.

这是一个前段时间我为GBA上的一个RPG游戏写的脚本引擎所使用的lex输入文件(部分)

例2.1

%{

/* need this for the call to atof() below */

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include "globals.h"

%}

digit        [0-9]

number       ("-"|"+")?{digit}+

hexnumber    "0x"({digit}|[a-fA-F])+

letter       [a-zA-Z]

identifier   ({letter}|_)({number}|{letter}|_)*

newline      [\n]

whitespace   [ \t]+

string       \"[^"]*\"

comment      "#"[^#]*"#"

%%

{string}     { return VM_STRING;        }

"Logo"       { return VMIN_LOGO; }

"FaceIn"     { return VMIN_FACEIN; }

"FaceOut"    { return VMIN_FACEOUT; }

"LoadTile"   { return VMIN_LOAD_TILE;   }

"CreateRole" { return VMIN_CREATE_ROLE; }

"ReleaseRole" { return VMIN_RELEASE_ROLE;}

"CreateMap"  { return VMIN_CREATE_MAP;  }

"ReleaseMAP" { return VMIN_RELEASE_MAP;}

"ShowBitmap" { return VMIN_SHOWBITMAP;  }

"CreateDialog" { return VMIN_CREATE_DIALOG; }

"ReleaseDialog" { return VMIN_RELEASE_DIALOG;}

"Fight"      { return VMIN_FIGHT;       }

"Delay"      { return VMIN_DELAY;       }

"PressA"     { return VMIN_PRESS_A;     }

"PressB"     { return VMIN_PRESS_B;     }

"PressR"     { return VMIN_PRESS_R;     }

"PressL"     { return VMIN_PRESS_L;     }

"PressStart" { return VMIN_PRESS_START; }

"PressSelect" { return VMIN_PRESS_SELECT;}

{number}     { return VM_NUMBER;        }

{whitespace} { /* skip whitespace */    }

{identifier} { return VM_ID;            }

{newline}    ;

.            ;

%%

int yywrap()

{

     return 1;

}

这里的lex输入文件一共有三个部分,用%%分开.第一部分中的%{和}%中的内容就是直接放在lex输出C代码中的顶部.我们通过它可以来定义一些所需要的宏,函数和include一些头文件等等.我的这个lex输入文件中也没什么特别的东西,就是常规的C源文件的include头文件

%{

/* need this for the call to atof() below */

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include "globals.h"

%}

第一部分中,除了前面的%{和}%包含的部分,下面的就是正则表达式的定义.

看了第一篇的正则表达式,这样你就能够在这里派上用场了.

让我们来看看我这里定义的正则表达式:

digit        [0-9]

number       ("-"|"+")?{digit}+

hexnumber    "0x"({digit}|[a-fA-F])+

letter       [a-zA-Z]

identifier   ({letter}|_)({number}|{letter}|_)*

newline      [\n]

whitespace   [ \t]+

string       \"[^"]*\"

comment      "#"[^#]*"#"

digit就不用说了,就是0-9的阿拉伯数字定义,第一篇文章中也举了这个例子.number就是digit的1到无限次的重复,再在其前面加上"+"和"-"符号.

注意:

"a": 即使a是元字符,它仍是字符a

\a: 当a是元字符时候,为字符a

a?: 一个可选的a,也就是说可以是a,也可以没有a

a|b: a或b

(a): a本身

[abc]: 字符a,b或c中的任一个

[a-d]: a,b,d或者d中的任一个

[^ab]: 除了a或b外的任何一个字符

.: 除了新行之外的任一个字符

{xxx}: 名字xxx表示的正则表达式

这里需要特别说明的就是

newline      [\n]

newline就是新行,这里我使用了[]把\n换行号括起来.因为如果我直接用\n表示的话,那么按照上面的规则,那就会看成\和n两个字符,所以我使用了[\n].有些时候newline也被写成[\n]|[\r\n].因为在文本文件中,一般换行一次,那么就是一个\n(0xA),可是在二进制文件中,换行有时候又是\r\n(0xD,0xA)一共两个字符号.

第二部分就是定义扫描到正则表达式的动作.

这些动作其实就是C代码,它们将会被镶嵌在lex输出的C文件中的yylex()函数中.

上面的例子的动作其实十分平常,就是返回一个值.

我们在外部使用这个lex为我们生成C代码的时候,只需要使用它的int yylex()函数.当我们使用一次yylex(),那么就会自动去扫描一个匹配的正则表达式,然后完成它相应的动作.这里的动作都是返回一值,那么yylex就会返回这个值.通常默认yylex返回0时候,表示文件扫描结束,所以你的动作中最好不要返回0,以免发生冲突.当然,动作中也可以不返回一值,那么yylex就会完成这个动作后自动扫描下一个可以被匹配的字符串,一直到扫描到文件结束.

当扫描到一个可以被匹配的字符串,那么这个时候,全局变量yytext就等于这个字符串

请大家一定记住这些正则表达式的顺序.

如果出现一个字符串,可以同时匹配多个正则表达式,那么它将会被定义在前面的正则表达式匹配.所以我一般把字符串string定义在最前面.

如果文件中的字符没有被lex输入文件中任何一个字符匹配,那么它会自动地被标准输出.所以大家一定要记住在每个正则表达式处理完毕后,一定要加上{newline}和.这两个正则表达式的动作.

好,让我们看看lex为我们输出C文件中提供一些常量

Lex 变量

yyin

FILE* 类型。 它指向 lexer 正在解析的当前文件。

yyout

FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。

yytext

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

yyleng

给出匹配模式的长度。

yylineno

提供当前的行数信息。(lexer不一定支持。)

例2.2

这是<<编译原理与实践>>书中配套的源代码的lex输入文件.大家可以参考一下,作者为它自己定义的一个Tiny C编译所做的词法扫描器.

/****************************************************/

/* File: tiny.l                                     */

/* Lex specification for TINY                       */

/* Compiler Construction: Principles and Practice   */

/* Kenneth C. Louden                                */

/****************************************************/

%{

#include "globals.h"

#include "util.h"

#include "scan.h"

/* lexeme of identifier or reserved word */

char tokenString[MAXTOKENLEN+1];

%}

digit       [0-9]

number      {digit}+

letter      [a-zA-Z]

identifier  {letter}+

newline     \n

whitespace  [ \t]+

%%

"if"            {return IF;}

"then"          {return THEN;}

"else"          {return ELSE;}

"end"           {return END;}

"repeat"        {return REPEAT;}

"until"         {return UNTIL;}

"read"          {return READ;}

"write"         {return WRITE;}

":="            {return ASSIGN;}

"="             {return EQ;}

"<"             {return LT;}

"+"             {return PLUS;}

"-"             {return MINUS;}

"*"             {return TIMES;}

"/"             {return OVER;}

"("             {return LPAREN;}

")"             {return RPAREN;}

";"             {return SEMI;}

{number}        {return NUM;}

{identifier}    {return ID;}

{newline}       {lineno++;}

{whitespace}    {/* skip whitespace */}

"{"             { char c;

                 do

                 { c = input();

                   if (c == EOF) break;

                   if (c == '\n') lineno++;

                 } while (c != '}');

               }

.               {return ERROR;}

%%

TokenType getToken(void)

{ static int firstTime = TRUE;

 TokenType currentToken;

 if (firstTime)

 { firstTime = FALSE;

   lineno++;

   yyin = source;

   yyout = listing;

 }

 currentToken = yylex();

 strncpy(tokenString,yytext,MAXTOKENLEN);

 if (TraceScan) {

   fprintf(listing,"\t%d: ",lineno);

   printToken(currentToken,tokenString);

 }

 return currentToken;

}

这里有点不同的就是,作者用了另外一个getToken函数来代替yylex作为外部输出函数.其中getToken里面也使用了lex默认的输出函数yylex(),同时还做了一些其它的事情.不过我建议大家不要像作者那样另外写自己的结果输出函数,因为在后面,需要和yacc搭配工作的时候,yacc生成的语法分析程序只认名字叫yylex()的词法结果输出函数.

if (firstTime)

 { firstTime = FALSE;

   lineno++;

   yyin = source;

   yyout = listing;

 }

其中的yyin,yyout,source,listing都是FILE*类型.yyin就是要lex生成的词法扫描程序要扫描的文件,yyout就是基本输出文件(其实我们通常都不用yyout,即使要生成一些输出信息,我们都是自己通过fprintf来输出).

"{"             { char c;

                 do

                 { c = input();

                   if (c == EOF) break;

                   if (c == '\n') lineno++;

                 } while (c != '}');

               }

其中,作者的这个Tiny C是以{}来包括注释信息.作者并没有写出注释信息的正则表达式,但是它可以通过检索"{",然后用lex内部函数input()一一检查 { 后面的字符是不是 } 来跳过注释文字.(C语言的/* */注释文字正则表达式十分难写,所以很多时候我们都用这种方法直接把它的DFA(扫描自动机)写出来).

本文就是通过简单地举出两个比较实际的例子来讲解flex输入文件的.再次说明,如果你是第一次接触lex,那么请看看前面我推荐的文章,你可以在IBM的开发者网上查到.下一篇关于yacc于BNF文法的说明也是如此.请大家先参考一下其它标准的教程.

---------------------------------------

从lex&yacc说到编译器(3.范式文法)

作者:tangl_99

QQ:8664220

msn:tangl_99@hotmail.com

email:tangl_99@sohu.com

从这一节开始,我们就算进入编译器构造的正题了.不得不说,前面的词法扫描器在整个编译器部分只是个很小很小的组成,而这两节讲述的语言构造器才能真正为我们的编译工作起到重要的作用.这些东西相信大家在大学的编译原理的课程已经学了不少,那么本文我也只是大致地带过,让大家回忆起大学的知识,重要的yacc使用技巧等等,我将在后面的内容讲出.

例3.1

exp -> exp op exp | (exp) | number

op -> + | - | *

这里就是一个定义的带有加法,减法,乘法的简单整数算术表达式的文法.其中粗体表示的是终结符号,也就是不能有产生式生成的符号.而exp,op就是非终结符,它们都是由一个"->"符号来产生的.

比如100 + 222 *123123 -(888+11)就是符合上述文法的具体的表达式.

注意,在文法定义中,是可以递归的.所以exp产生式右边的式子中可以再次出现exp.

这里的|和正则表达式一样,表示的选择的意思,也就是说,exp可以是exp op exp或者(exp)再或者number.

下面让我们看看<<编译原理及实践>>书中的一个关于BNF文法的介绍.

比如说我们有个数学表达式(34-3)*42,然后我们来看看上面的exp文法怎么来推导识别它.

(1) exp => exp op exp               [exp ->exp op exp]

(2)     => exp op number            [exp ->number    ]

(3)     => exp * number             [op -> *         ]

(4)     => (exp) * number           [exp ->(exp)     ]

(5)     => (exp op exp) * number    [exp ->exp op exp]

(6)     => (exp op number)* number  [exp -> number   ]

(7)     => (exp - number) * number [op -> -         ]

(8)     => (number-number)* number [exp -> number   ]

最终,exp里面全部的非终结符号全部变成了终结符号.那么推导完成.

这种推导十分像我们在离散数学中讲到的命题推理.其实形式语言的推导的数学基础就是我们离散数学的命题推理.

在推导过程中,其实就是把原来的文法中的递归展开.那么我们在推导的过程,也就很容易实现分析树的生成.而分析树就是我们编译程序中十分重要的信息源.我们之所以前面又做词法分析,又做语法分析的目标就是为了生成分析树.有了它,我们编译程序在后面的代码生成过程中将变得容易百倍.

请看:

?

例3.2

同样是<<编译原理及实践>>书上的例子.

设E -> E+a | a 表示的文法为G,那么考虑它生成的表达L(G)

如果由标准的数学定义,那么我们用公式L(G)={s | exp =>* s }表示一种文法G.

s代表记号符号的任意数组串,也就是我们的终结符号.而exp代表非终结符号,=>*表示一系列的从非终结符到终结符号的推导过程.这里*有点像我们在讲述正则表达式中的*符号一样,它表示0到无限次的重复.所以=>*就是表示0次到无限次的推导过程.

L(G) = {a,a+a,a+a+a,a+a+a+a,…}

E => E+a => E+a+a => E+a+a+a

同时,在我们的编译课本上,又经常讲述另一种数学表达方式来阐述文法的定义.

G=(T,N,P,S)

注意,这里的T,N,P,S都是集合.

T表示终结符号(terminal),也就是这里{a,+}

N表示非终结符号(nonterminal),也就是这里{E},但是N不能与T相交.

P表示产生式(production)或者文法规则(grammar rule)的集合,这里它只有一个元素: E -> E+a

S表示集合N的开始符号(start symbol).关于S,本人也搞不清楚它的用处,所以很抱歉!

例3.3

这是我们C程序语言中经常使用if else文法

statement -> if-stmt | other

if-stmt -> if (exp) statement | if (exp) statement else statement

exp -> 0|1

statement就是我们C语言中使用语句,它的产生式包括了两种可能,一是if-stmt语句,二是other.然后我们又另外定义if-stmt语句的产生式.这里有两种情况,一是没有else的,另外就是有else的.里面我们又使用了递归.if-stmt本来是包含在statement里面的,可是我们又在if-stmt的产生式中使用statement.正是因为文法中允许递归,所以它比起我们前面讲的正则表达式有更广泛的表示能力,但同时,文法的推导识别也更加法复杂.按照编译原理的书籍,一般讲完BNF文法后,就要重点讲解文法的推导算法.一共有两种,一是LL算法,自顶向下的算法,二是LR算法,自底向上的算法.LL算法比较简单,其中还有一种特殊的情况,就是我们下一节要讲的递归下降的算法.由于C语言中的函数本来就可以递归,那么实现这中递归下降的算法是十分简单的,而且对于我们一般的程序设计语言来说,虽然它的算法能力很弱,但是已经是足够用了.而关于LR的算法,那么就是一个大难题了.它的算法能力最强,但是实现起来十分困难,还好,已经有科学家为我们提供了yacc(或者叫bison)这个工具,可以来自动生成LR的文法推导算法.这就是我们一直在提到的yacc工具了.

回过头来,我们看看下面的程序

if(0) other else other

的分析树

?

思考: 为什么要把文法最终分析成树结构?

因为文法本身是递归的,而表示的递归的最好数据结构就是树,所以我们把文法弄成树结构后,后面在处理代码生成等问题上,也可以用递归来很容易地完成.

例3.4

这里我给出microsoft在msdn中对于C语言的statement的文法

注意,这里用:符号替代了我们前面产生式的->

statement :

labeled-statement

compound-statement

expression-statement

selection-statement

iteration-statement

jump-statement

try-except-statement   /* Microsoft Specific */

try-finally-statement   /* Microsoft Specific */

jump-statement :

goto identifier ;

continue;

break;

return expression opt ;

compound-statement :

{ declaration-list opt statement-list opt }

declaration-list :

declaration

declaration-list declaration

statement-list :

statement

statement-list statement

expression-statement :

expression opt ;

iteration-statement :

while ( expression ) statement

do statement while ( expression );

for ( expression opt ; expression opt ; expression opt ) statement

selection-statement :

if ( expression ) statement

if ( expression ) statement else statement

switch ( expression ) statement

labeled-statement :

identifier : statement

case constant-expression : statement

default : statement

try-except-statement :   /* Microsoft Specific */

__try compound-statement

__except ( expression ) compound-statement

try-finally-statement :   /* Microsoft Specific */

__try compound-statement

__finally compound-statement

2003-10-11

                                               成都,四川大学