多谢各位的一直以来的支持,我们今天总算走到了实践的一步。今天我们要用VBF.Compilers的词法分析库来开发一个小型语言——miniSharp的词法分析。miniSharp是C#语言的子集,miniSharp程序的语义就等于把它当做C#的语义。但是miniSharp只支持很少的语言特性,以降低制作编译器的难度。简单来说miniSharp有如下特征:

  1. 只有一个源文件,不能引用其他dll(甚至不能引用.NET的类库)。
  2. 没有命名空间。
  3. 第一个类必须是静态类,而且里面只能定义一个静态方法Main作为程序入口。
  4. 只能定义类,没有枚举、结构体、接口、委托等。
  5. 类的成员只有私有的字段和共有的非静态方法两种。不支持虚方法。
  6. 方法必须有返回值,除了Main方法之外。
  7. 支持的类型只有int、bool、int[]和自定义的类。不支持其他类型。
  8. 仅支持一个库函数System.Console.WriteLine,只支持参数是int的用法。
  9. 只支持if-else语句、while语句、赋值语句、变量声明语句和调用WriteLine语句。
  10. 只支持+、-、*、/、>、<、==、&&、||和!运算符
  11. 每个方法只能有一个return语句,必须是方法最后一条语句。
  12. 其他C#特性皆不支持。

大家肯定觉得这个语言“阉割”得实在太厉害了,我感兴趣的泛型、Lambda表达式、Linq啥的统统都不支持,还写个什么劲呀。但是我劝告各位不要一口吃个胖子。如果写大型语言,会耗费很大的经历在语法分析、语义分析这两步上,甚至可能会遇到困扰很久的问题,导致我们不能很快地体验编译器后端的技术。所以咱们先从简单的语言开始,一步一步来。基本原理都是一样的,等大家熟悉之后自然就可以自己往里面加入任何想加的特性。注:miniSharp设计参考了虎书Java版中的miniJava语言。

 

今天我们首先来看miniSharp的词法分析。miniSharp语言的单词根据优先级和不同种类可以分成以下五类:

  1. 关键字
  2. 标识符
  3. 整型数字常量
  4. 各种标点符号
  5. 空白符、换行符和注释

关键字大家都好理解。标识符是有必要仔细考虑的单词,因为我们希望miniSharp像C#一样支持用中文做变量名或函数名,所以肯定不能使用“下划线或字母开头,后面跟下划线、字母或数字”这样的定义。参考C#语言规范,我们要用Unicode字符分类来定义标识符。后面整型、标点符号什么的无需多说,最后我们要讨论一下空白符、换行符和注释的词法规则。

 

先从简单的开始,我们要为miniSharp中每一种关键字创建一个单词类型。这些关键字都不能用作标识符,所以都是保留字。所有关键字的正则表达式都是一串字符的连接运算,所以我们直接用RegularExpression的Literal方法来定义:

 

 
var lex = lexicon.DefaultLexer;

//keywords
K_CLASS = lex.DefineToken(RE.Literal("class"));
K_PUBLIC = lex.DefineToken(RE.Literal("public"));
K_STATIC = lex.DefineToken(RE.Literal("static"));
K_VOID = lex.DefineToken(RE.Literal("void"));
K_MAIN = lex.DefineToken(RE.Literal("Main"));
K_STRING = lex.DefineToken(RE.Literal("string"));
K_RETURN = lex.DefineToken(RE.Literal("return"));
K_INT = lex.DefineToken(RE.Literal("int"));
K_BOOL = lex.DefineToken(RE.Literal("bool"));
K_IF = lex.DefineToken(RE.Literal("if"));
K_ELSE = lex.DefineToken(RE.Literal("else"));
K_WHILE = lex.DefineToken(RE.Literal("while"));
K_SYSTEM = lex.DefineToken(RE.Literal("System"));
K_CONSOLE = lex.DefineToken(RE.Literal("Console"));
K_WRITELINE = lex.DefineToken(RE.Literal("WriteLine"));
K_LENGTH = lex.DefineToken(RE.Literal("Length"));
K_TRUE = lex.DefineToken(RE.Literal("true"));
K_FALSE = lex.DefineToken(RE.Literal("false"));
K_THIS = lex.DefineToken(RE.Literal("this"));
K_NEW = lex.DefineToken(RE.Literal("new"));

其中的lexicon是我们上一回介绍的Lexicon类创建的实例。

 

接下来我们重点来看标识符的词法。我们不支持C#中@开头的标识符,所以只考虑一种情况。C# Spec规定标识符开头字符必须是一个“字母类”字符或者下划线“_”字符。其中“字母类”并非只是大小写字符,而是Unicode分类中的Lu、Ll、Lt、Lm、Lo、Nl这些类别的字符。含义分别如下:

  1. Lu表示大写字母,包含所有语言中的大写字母。
  2. Ll表示小写字母,包含所有语言中的小写字母。
  3. Lt表示所有词首大写字母(titlecase)。
  4. Lm表示所有修饰字母(modifier)。
  5. Lo表示其他字母,如中文、日文的字符。
  6. Nl表示数字,但不是十进制数字,而是字母表示的。比如罗马数字。

标识符第二个字符开始,允许“字母类”字符和下划线以外,还允许以下类型的字符:

  1. 组合类字符,Unicode分类Mn和Mc
  2. 十进制数字,Unicode分类Nd
  3. 连接类字符,Unicode分类Pc
  4. 格式类字符,Unicode分类Cf

用VBF.Compilers.Scanners类库时,可以使用RegularExpression.CharsOf方法,借助Lambda表达式来生成Unicode字符的并集。目前我的设计处理这一块不是十分高效,所以miniSharp的词法就稍微简化一点,允许以字母类的字符或下划线开头,然后零个或多个字母类字符、下划线或数字,也即不支持上述定义中组合类、连接类和格式类字符。定义标识符的正则表达式写法如下:

 

 
var lettersCategories = new[] 
{ 
    UnicodeCategory.LetterNumber,
    UnicodeCategory.LowercaseLetter,
    UnicodeCategory.ModifierLetter,
    UnicodeCategory.OtherLetter,
    UnicodeCategory.TitlecaseLetter,
    UnicodeCategory.UppercaseLetter
};

var RE_IdChar = RE.CharsOf(c => lettersCategories.Contains(Char.GetUnicodeCategory(c))) | RE.Symbol('_');

ID = lex.DefineToken(RE_IdChar >>
    (RE_IdChar | RE.Range('0', '9')).Many(), "identifier");

 

大家可以看到我用了.NET类库中的Char.GetUnicodeCategory方法来判断Unicode分类。将来的VBF类库中可能会提供Unicode分类的直接支持。接下来是整型常量和标点符号,没有啥好说的,直接看代码:

 

INTEGER_LITERAL = lex.DefineToken(RE.Range('0', '9').Many1(), "integer literal");

//symbols

LOGICAL_AND = lex.DefineToken(RE.Literal("&&"));
LOGICAL_OR = lex.DefineToken(RE.Literal("||"));
LOGICAL_NOT = lex.DefineToken(RE.Symbol('!'));
LESS = lex.DefineToken(RE.Symbol('<'));
GREATER = lex.DefineToken(RE.Symbol('>'));
EQUAL = lex.DefineToken(RE.Literal("=="));
ASSIGN = lex.DefineToken(RE.Symbol('='));
PLUS = lex.DefineToken(RE.Symbol('+'));
MINUS = lex.DefineToken(RE.Symbol('-'));
ASTERISK = lex.DefineToken(RE.Symbol('*'));
SLASH = lex.DefineToken(RE.Symbol('/'));
LEFT_PH = lex.DefineToken(RE.Symbol('('));
RIGHT_PH = lex.DefineToken(RE.Symbol(')'));
LEFT_BK = lex.DefineToken(RE.Symbol('['));
RIGHT_BK = lex.DefineToken(RE.Symbol(']'));
LEFT_BR = lex.DefineToken(RE.Symbol('{'));
RIGHT_BR = lex.DefineToken(RE.Symbol('}'));
COMMA = lex.DefineToken(RE.Symbol(','));
COLON = lex.DefineToken(RE.Symbol(':'));
SEMICOLON = lex.DefineToken(RE.Symbol(';'));
DOT = lex.DefineToken(RE.Symbol('.'));

 

稍微说明一点,整型常量和上面的标识符的词法,在调用lex.DefineToken时都多传了一个参数。这个参数是可选的描述信息,如果不传会直接使用正则表达式的字符串形式。而标识符的正则表达式有4万多个字符那么长而且没有可读性,所以加一个额外字符串描述一下。它将来会被用于生成编译错误信息。

 

最后我们来写空白符、换行符和注释的正则表达式。这三个是完全按照C# spec的规范编写的。其中注释包含了两种://开头直到换行的注释已经/*开头直到*/的多行注释。大家可以学习一下它们的正则表达式怎么写:

 

var RE_SpaceChar = RE.CharsOf(c => Char.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator);

WHITESPACE = lex.DefineToken(RE_SpaceChar | RE.CharSet("\u0009\u000B\u000C"));

LINE_BREAKER = lex.DefineToken(
    RE.CharSet("\u000D\u000A\u0085\u2028\u2029") |
    RE.Literal("\r\n")
);

var RE_InputChar = RE.CharsOf(c => !"\u000D\u000A\u0085\u2028\u2029".Contains(c));
var RE_NotSlashOrAsterisk = RE.CharsOf(c => !"/*".Contains(c));
var RE_DelimitedCommentSection = RE.Symbol('/') | (RE.Symbol('*').Many() >> RE_NotSlashOrAsterisk);

COMMENT = lex.DefineToken(
    (RE.Literal("//") >> RE_InputChar.Many()) |
    (RE.Literal("/*") >> RE_DelimitedCommentSection.Many() >> RE.Symbol('*').Many1() >> RE.Symbol('/'))
);

 

最后还有一点后续的代码,从Lexicon对象生成ScannerInfo,再生成Scanner:

 

ScannerInfo info = lexicon.CreateScannerInfo();
Scanner scanner = new Scanner(info);

string source = "//任意miniSharp源代码";
StringReader sr = new StringReader(source);

scanner.SetSource(new SourceReader(sr));
scanner.SetSkipTokens(WHITESPACE.Index, LINE_BREAKER.Index, COMMENT.Index);

 

这样就完成了!我们创建了一个完整的miniSharp词法分析器。现在它就能分析所有miniSharp源代码了。注意我们设定了该词法分析器忽略所有空白符、换行以及注释,是为了后面语法分析简便而考虑的。各位读者可以自己试着任意扩展这个词法分析器,比如增加字符串常量的词法、更多关键字和运算符甚至前所未有的新词法。祝各位实践愉快!下一篇开始我们要进入另一个重要的环节——语法分析部分,敬请期待。

此外别忘了关注我的VBF项目:https://github.com/Ninputer/VBF 和我的微博:http://weibo.com/ninputer 多谢大家支持!

 posted on 2011-06-13 21:54  装配脑袋  阅读(17292)  评论(19编辑  收藏  举报