我总是说在学习一项新技术的时候示例总是不够简单. 这就是为什么我在试图做一个最简单的编译器的时候还是把所有的特性都做进去了. 那么我开始从一个具有象C语言那样的语法风格和Basic那样的功能特性的字符处理的程序环境说起。下面是一个在我们的语言里面可以正确运行的程序。
input name;
if (name == " Jan " ) { // string comparison
name = "my creator"; // string assignment
happy = "yes";
}
print " Thank you, " + name + " !\n " + // string concatenation
" You've just made a simple program very happy! " ;
正如你所看到的那样,这些代码里面没有象函数,类等那样的语句块式结构,它甚至没有数字型的类型。当然,最终的产品可以很容易被扩展。
但是在达到那个目标之前,我们是不是仍需多花点时间回忆一下我们上次列出的那些组件?今天我们会实现第一个 : 词法分析器或语法缩写。这是个很好的热身运动,因为它在我们编译器中真的不是很困难的一个部分。
废话少说,我们现在就开始操练。
是什么,为什么,怎么做首先,我猜你肯定想知道词法分析器到底是个什么东西?我们为什么需要这样一个东西呢?词法分析器的作用是把输入的字符串流转换为包含一定意义的信息流。实际上它只是检查字符串并从它们里面识别出关键字。
我们当然会想到写一个方法,把当前的词与我们定义好的关键字列表作一一的比较匹配,显而易见,这样的速度是很慢的。那么我们应该怎么办呢?我们使用一个叫"有限状态机"(finite state machine)的东西来帮助我们识别这些关键字(如果你不知道那是个什么东西,别担心,你不需要知道)。
关于词法匹配逻辑的问题最让人高兴的是我们实际上不需要做那些枯燥无味的工作,一个叫做"Lex"的小工具会帮助我们完成定义词法逻辑的工作。这是一个Unix下面的标准程序,Win32平台下也有一个类似的东西(我所用的那个叫做"Flex",我已经打包在这部分的文件包中了)。需要Lex完整的HTML用户手册,请看这里。
好了,现在你知道词法匹配逻辑是个什么东西,我们要开始制作它了。接下来我们需要去下载一个文件tut2.zip并看看它的代码。这部分源代码是string.l和main.cpp, 再加上一些预包含的文件。注意,这个压缩包里文件组织方式是与我们的系列讲座相关的。例如,flex.exe位于根目录下,而关于本次的讲座的文件则在tut2\目录下。
词法分析器规则Lex需要输入一些简单的示例规则来创建我们的词法逻辑。在我对这些规则作解释之前我们先来学习一下Lex的输出文件的结构。
%%
< rules >
%%
< user_code >
这部分的内容包含一些正则表达式的宏(Lex的用户手册里面解释了什么是正则表达式,欲知更多信息请看这里)。
这些内容告诉Lex我们如何定义数字,字母,标识符(标识符,被定义成一个字母后面跟零或者更多的字母和数字)以及一个STR(字符常量,指包含在双引号内的字符串)。
这部分内容也可以包含一些初始化的代码象#includces这样的预包含文件和前置引用申明。这些代码应该被放在%{ 和 %}之间。它括一个叫lexsymb.h的预包含文件,我们马上会谈到它。
lexsymb.h文件包含着词法逻辑分析器关于它将会返回的表意符号的申明。它也包含一个叫yylval字符串(有点象一个标识符的名字)的定义申明,它可以用来给调用者传递额外的信息。我们使用这个特定字符串的原因我们将在下一部分中讨论。
现在,让我们来看看真正的规则。请注意,我使用/* */来作注释;Lex是古老的程序,因此不支持使用//注释其中的输入内容。我们将会创建一个基于C语言版本的逻辑分析器,顺便说一下,虽然有现成的C++版本的程序,但是Unix下面的逻辑分析器是生成C代码的,我们总是希望它简单一些,不是吗?
" = " {return ASSIGN;}
" ; " {return END_STMT;}
{IDENT} {Identifier (); /**//* identifier: copy name */
return ID;}
{STR} {StringConstant (); /**//* string constant: copy contents */
return STRING;}
" // " {EatComment();} /**/ /* comment: skip */
\n {lineno++;} /**/ /* newline: count lines */
{WSPACE} {} /**/ /* whitespace: (do nothing) */
. {return ERROR_TOKEN;} /**/ /* other char: error, illegal token */
我只列出了部分的规则,因为它们看起来都差不多,正如你所看到的那样,每条规则都是以Lex可以识别的模式开头,后面跟一些告诉Lex要完成的动作(顺便地,"可以"包括C++的代码,因为Lex只是把它们在输出文件里原样输出). 有时非常需要注意的是越是靠上的规则越是被优先应用.
最前的三条规则是非常简单的;它们只是识别出一个字符串然后返回一个有意义的符号给调用者.注意,如果你需要把:=作为赋值符号的话只需要简单地替换就可以了.
下面的一行是第一个比较有意思的地方 : 它使用IDENT宏,因此它可以识别不满面足已存在规则的数字或者字符的字串。如果找到了,那么它呼叫Identifier() 这个函数,此函数会把这个字串从yytext(它包含与当前标识相关的字符串)拷到一个新的字符数组中。逻辑分析器返回这个ID的标识;调用程序只需注意'yylval->str‘这个指针指向的标识符。STR也同样地用于一个字符串常量。
下面的行用于识别注释内容,新行开始和白空格。注意,我们的行标识是个递增的量,这样它可以用来帮助我们定位错误语句的位置,这个我们在稍后会谈到。最后面的一条规则则指明了如果我们发现了不能匹配任何已知规则约束的时候Lex应该如何处理,我们应该叫它返回一个错误信息并告知调用者怎么办(这里的正则表达式"."表示除了"\n"以外的任何字符)。
一个Lex的输入文件可以使用下面的命令来编译输出到一个叫lex.cpp 的文件 :
flex -olex.cpp string.l
在MSVB 6.0(string.dsp)中.ZIP格式的压缩包也可以做为项目文件。我信息在5.0中也是这样,但是我们不敢肯定。工程文件里面包含有编译string.l的用户自定义命令,所以它完全可以被自动编译。
、不幸的是,lex使用非标准的包含文件:unistd.h,它不可以在Windows系统中使用。在根目录中那是个空的unistd.h文件;包括你的包含文件根目录(MSVC :Tools->Options->Directories->Include).
它真的变得越来越棒了!好了,我们现在有了一个可以“读”的程序了。不幸的是,它仍然不知道它到底是读取的什么东西和它是否符合我们的标准。它只是接受它可以识别的标识。
因此它还需要了解语法。相当另人惊喜的是,语法正是我们在下一部分中要讨论的内容。下面一个部分叫做解析器。它的工作是梳理出程序结构并且检查它是否满足语法结构。
我们的工作好象越来越有趣了,千万别走开哦。我们的编译器已经可以接受我们给它的输入了,并且可以识别语法的正确性而不是照单全收!我知道你们跟我一样兴奋,但是我们还是必需等待下一个部分的内容......
引用
只有当电脑可以被装在口袋的时代到来的时候,下面的事才会最终变成现实:
世界上所有报纸上的内容都不会遵守同样的数学定律正如所有写在餐馆帐单上的内容一样。
简单的事实也会在自然科学世界引发一场风暴,它将被砌底颠覆。正因为在这样高档的餐厅里面举行如此多的数学研讨会,警察们格外关心由心脏病引的死亡,而数学科学也因此滞后多年。