多谢各位的一直以来的支持,我们今天总算走到了实践的一步。今天我们要用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,再生成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 多谢大家支持!