zoukankan      html  css  js  c++  java
  • 自己动手开发编译器(五)miniSharp语言的词法分析器

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

  • 相关阅读:
    MySQL5.7.17解压版安装
    autocomplete初步使用
    前端面试题:驼峰体与匈牙利语法的相互转换
    前端常用正则表达式
    解决npm报错:Module build failed: TypeError: this.getResolve is not a function
    vue实现对语言的切换,结合vue-il8n。
    大量数据处理的一个思路
    不同格式矢量数据源在MapServer上发布服务后切片缓存效率对比
    CentOS7使用yum安装PostgreSQL和PostGIS
    ArcGIS消除图斑重叠错误
  • 原文地址:https://www.cnblogs.com/Ninputer/p/2080094.html
Copyright © 2011-2022 走看看