自己动手开发编译器(五)miniSharp语言的词法分析器
多谢各位的一直以来的支持我们今天总算走到了实践的一步。今天我们要用VBF.Compilers的词法分析库来开发一个小型语言——miniSharp的词法分析。miniSharp是C#语言的子集miniSharp程序的语义就等于把它当做C#的语义。但是miniSharp只支持很少的语言特性以降低制作编译器的难度。简单来说miniSharp有如下特征只有一个源文件不能引用其他dll甚至不能引用.NET的类库。没有命名空间。第一个类必须是静态类而且里面只能定义一个静态方法Main作为程序入口。只能定义类没有枚举、结构体、接口、委托等。类的成员只有私有的字段和共有的非静态方法两种。不支持虚方法。方法必须有返回值除了Main方法之外。支持的类型只有int、bool、int[]和自定义的类。不支持其他类型。仅支持一个库函数System.Console.WriteLine只支持参数是int的用法。只支持if-else语句、while语句、赋值语句、变量声明语句和调用WriteLine语句。只支持、-、*、/、、、、、||和!运算符每个方法只能有一个return语句必须是方法最后一条语句。其他C#特性皆不支持。大家肯定觉得这个语言“阉割”得实在太厉害了我感兴趣的泛型、Lambda表达式、Linq啥的统统都不支持还写个什么劲呀。但是我劝告各位不要一口吃个胖子。如果写大型语言会耗费很大的经历在语法分析、语义分析这两步上甚至可能会遇到困扰很久的问题导致我们不能很快地体验编译器后端的技术。所以咱们先从简单的语言开始一步一步来。基本原理都是一样的等大家熟悉之后自然就可以自己往里面加入任何想加的特性。注miniSharp设计参考了虎书Java版中的miniJava语言。今天我们首先来看miniSharp的词法分析。miniSharp语言的单词根据优先级和不同种类可以分成以下五类关键字标识符整型数字常量各种标点符号空白符、换行符和注释关键字大家都好理解。标识符是有必要仔细考虑的单词因为我们希望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这些类别的字符。含义分别如下Lu表示大写字母包含所有语言中的大写字母。Ll表示小写字母包含所有语言中的小写字母。Lt表示所有词首大写字母titlecase。Lm表示所有修饰字母modifier。Lo表示其他字母如中文、日文的字符。Nl表示数字但不是十进制数字而是字母表示的。比如罗马数字。标识符第二个字符开始允许“字母类”字符和下划线以外还允许以下类型的字符组合类字符Unicode分类Mn和Mc十进制数字Unicode分类Nd连接类字符Unicode分类Pc格式类字符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再生成ScannerScannerInfo 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);