前言
当我们熟悉了如何构建起自己的语言服务之后,剩下的问题就是真正的完成扫描和解析,任何一个优秀的语言服务都少不了优秀的扫描程序和解析程序。编写扫描程序和解析程序有很多种方式,我想Lex和Yacc是比较常用的,否则微软也不会去实现一个基于C#的Lex和Yacc(我指的是MPLEX 和 MPPG)。俗话说磨刀不误砍柴,我们先来了解一下Lex和Yacc。本文的部分内容摘自:http://www.ibm.com/developerworks/cn/linux/sdk/lex/
Lex和Yacc简介
Lex 代表 Lexical Analyzar,Yacc 代表 Yet Another Compiler Compiler,因此它们并不是某种语言,Lex 和 Yacc 是 UNIX 两个非常重要的、功能强大的工具。事实上,如果你熟练掌握 Lex 和 Yacc 的话,它们的强大功能使创建 FORTRAN 和 C 的编译器如同儿戏。很重要的概念是,在Linux下,flex取代了Lex ,bison取代了Yacc ;在LS设计时用的是MPLEX 和 MPPG。我们写的源代码称为“具有lex和yacc风格的代码”。
Lex
概述
Lex 是一种生成扫描器的工具。当 Lex 接收到文件或文本形式的输入时,它试图将文本与常规表达式进行匹配。它一次读入一个输入字符,直到找到一个匹配的模式。如果能够找到一个匹配的模式,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 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。这些段以%%来分界。 那么,来看看前一篇的lex代码。
%using Babel; %using Babel.Parser; %namespace Babel.Lexer %% [0-9]+ {return (int)Tokens.NUMBER;} [a-z]+ {return (int)Tokens.LOWLETTER;} [A-Z]+ {return (int)Tokens.CAPLETTER;} . ; %% /* .... */
代码被两组%%分割成三部分,第一部分的名字空间声明取代了C的头文件声明,中间由常规表达式和C#代码组成,表示当某种模式匹配时执行相应的动作。最后一部分是C#的补充代码这里没有内容。我们也可以使用标记声明来修改一下代码:
%using Babel; %using Babel.Parser; %namespace Babel.Lexer Number [0-9] Lowletter [a-z] Capletter [A-z] AnyCharacter [.\n] %% {Number}+ {return (int)Tokens.NUMBER;} {Lowletter}+ {return (int)Tokens.LOWLETTER;} {Capletter}+ {return (int)Tokens.CAPLETTER;} {AnyCharacter} ; %% /* .... */
可以看到,我用了一些类似变量的东西,在第一段代码中声明了一些模式,这些“变量”称为标记声明。在第二段中可以使用{}引用这些标记声明。另外,在第一段的声明部分可以用%{和%}划分一个C#代码段,其中的代码可以是C#代码,MPLex会拷贝这部分代码到目标文件中,不会做任何解析和改动;在第一段中还可以声明一些状态,这些状态可以被扫描器使用,在高级的应用中几乎避免不了使用状态。
Yacc
概述
它是一种工具,将任何一种编程语言的所有语法翻译成针对此种语言的 Yacc 语法解析器。它用巴科斯范式(BNF, Backus Naur Form)来书写。按照惯例,Yacc 文件有 .y 后缀,在MPPG识别的也是.y后缀的文件。
相关概念
语法
在进一步阐述以前,考虑一下什么是语法。在上一节中,我们看到 Lex 从输入序列中识别标记。如果你在查看标记序列,你可能想在这一序列出现时执行某一动作。这种情况下有效序列的规范称为语法。Yacc 语法文件包括这一语法规范。它还包含了序列匹配时你想要做的事。为了更加说清这一概念,让我们以英语为例。 这一套标记可能是:名词, 动词, 形容词等等。为了使用这些标记造一个语法正确的句子,你的结构必须符合一定的规则。一个简单的句子可能是名词+动词或者名词+动词+名词。(如 I care. See spot run.) 。所以在我们这里,标记本身来自语言(Lex),并且标记序列允许用 Yacc 来指定这些标记(标记序列也叫语法)。
终结符
代表一类在语法结构上等效的标记。终结符号有三种类型:
命名标记: 这些由 %token 标识符来定义。按照惯例,它们都是大写。
字符标记 : 字符常量的写法与 C 相同。例如, -- 就是一个字符标记。
字符串标记 : 写法与 C 的字符串常量相同。例如,"<<" 就是一个字符串标记。
lex 返回命名标记。
非终结符
是一组非终结符和终结符组成的符号。按照惯例,它们都是小写。
Yacc编程
如同 Lex 一样, 一个 Yacc 程序也用双百分号分为三段。它们是:声明、语法规则和 C 代码。
回顾我们之前的代码
%using Microsoft.VisualStudio.TextManager.Interop %namespace Babel.Parser %valuetype LexValue %partial /* %expect 5 */ %union { public string str; } %{ ErrorHandler handler = null; public void SetHandler(ErrorHandler hdlr) { handler = hdlr; } internal void CallHdlr(string msg, LexLocation val) { handler.AddError(msg, val.sLin, val.sCol, val.eCol - val.sCol); } %} %token NUMBER %token CAPLETTER %token LOWLETTER %% Program : Declarations ; Declarations : Declarations Declaration | Declaration ; Declaration : NUMBER | CAPLETTER | LOWLETTER ; %%
上面的代码中,第一部分声明包含了很多内容,暂不做解释,只来看一下类似%token NUMBER的部分,这部分也称为标记声明,所不同的是这里的标记声明是lex和yacc共有的标记声明,lex返回的标记,必须是这里声明过的!注意到在lex的代码中return的标记是这里声明过的。第二段是语法声明,无论你是否了解巴科斯范式,应该能感觉到这像是一颗“树”。顶层是Program,由它会引伸出许多枝节,枝节再生枝节,最后的叶子往往是第一部分定义的标记。
Declaration : NUMBER | CAPLETTER | LOWLETTER ;
看上面这个表达,这个表达表示的是:一个Declaration可以由一个NUMBER或者一个CAPLETTER 或者一个LOWLETTER 组成。这里的NUMBER、CAPLETTER、LOWLETTER 就成为终结符,Declaration就称为非终结符。可见,一个非终结符可以有终结符或非终结符构成。另外第二部分也可以像lex的第二部分那样在某种序列匹配的时候定义一些动作,这里没有定义任何动作,在我们真正的应用中像括号匹配、错误检查等功能都是通过在这里定义动作实现的。
Lex和Yacc结合
两者结合的关键便是%token,在parser.y的输出文件parser.cs中,这些定义的标记被编译成Tokens类型的枚举:
public enum Tokens { error=1,EOF=2,NUMBER=3,CAPLETTER=4,LOWLETTER=5};
枚举值为1和2是默认的两种标记,我们自定义的标记是从3开始的。error标记常用于默认的错误处理情况,我们也可以在“语法树”中将其当做终结符使用,这样可以使我们的语言服务更健壮,并提供特殊的错误处理方式。
我强烈建议在开发语言服务时只使用命名标记而避免使用字符标记和字符串标记。
Lex和Yacc的合作还体现在其他地方,以后读者可以自己体会。
小结
本篇介绍了Lex和Yacc,并结合了上一个例子的代码阐述了Lex和Yacc编程的基本模式和基本语法。微软的MPLex和MPPG几乎实现了完整的Lex和Yacc,但是有些地方稍有扩展,相应的文档可以在C:\Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\ExtraDocumentation路径下找到。我正在试图翻译其中的文档,在以后的章节中,我会把翻译结果发上来。想要很好的掌握MPLex和MPPG,这些文档是必看的。另外,我建议初学者先结合一些资料进行一些基于C的Lex和Yacc开发,在熟悉lex和yacc后再开始语言服务的开发。我的博文在Visual Studio2008中搭建lex和yacc调试环境可以帮助大家搭建编程环境。读者也可以尝试阅读ManagedMyC的lex和yacc,其中一些表达方式是很值得学习的。
其他Lex和Yacc资源:
http://www.ibm.com/developerworks/cn/linux/sdk/lex/
熊春雷:《Lex和Yacc从入门到精通》
O’REILLY 《lex与yacc第二版》