惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
C
CERT Recently Published Vulnerability Notes
P
Proofpoint News Feed
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Help Net Security
Help Net Security
月光博客
月光博客
N
News and Events Feed by Topic
Cloudbric
Cloudbric
博客园 - 司徒正美
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tenable Blog
The Register - Security
The Register - Security
The Hacker News
The Hacker News
I
InfoQ
The Last Watchdog
The Last Watchdog
MyScale Blog
MyScale Blog
Schneier on Security
Schneier on Security
WordPress大学
WordPress大学
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
F
Fortinet All Blogs
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
大猫的无限游戏
大猫的无限游戏
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
S
Schneier on Security
C
Cisco Blogs
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Latest news
Latest news
PCI Perspectives
PCI Perspectives
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
T
Tor Project blog
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Webroot Blog
Webroot Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

博客园 - 装配脑袋

Windows Azure一些小技巧集合 C++ AMP实战:绘制曼德勃罗特集图像 GPU并行计算版函数图像生成器 自己动手开发编译器(十二)生成托管代码 自己动手开发编译器(十一)语义分析 自己动手开发编译器(十)miniSharp语法分析器 自己动手开发编译器(九)CPS风格的解析器组合子 自己动手开发编译器(八)用Linq编写解析器组合子 自己动手开发编译器(七)递归下降的语法分析器 自己动手开发编译器(六)上下文无关语言和文法 自己动手开发编译器特别篇——用词法分析器解决背诵圣经问题 自己动手开发编译器(四)利用DFA转换表建立扫描器 自己动手开发编译器(三)有穷自动机 自己动手开发编译器(二)正则语言和正则表达式 自己动手开发编译器(一)编译器的模块化工程 自己动手开发编译器(零)序言 DirectCompute & DirectX 11 计算着色器编程简介(翻译) 趣味问题:你能用Reflection.Emit生成这段代码吗?(答案) 趣味问题:你能用Reflection.Emit生成这段代码吗?
自己动手开发编译器(五)miniSharp语言的词法分析器
装配脑袋 · 2011-06-13 · via 博客园 - 装配脑袋

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