文章中引用的代码均来自https://github.com/vczh/tinymoe

 

实现Tinymoe的第一步自然是一个词法分析器。词法分析其所作的事情很简单,就是把一份代码分割成若干个token,记录下他们所在文件的位置,以及丢掉不必要的信息。但是Tinymoe是一个按行分割的语言,自然token列表也就是二维的,第一维是行,第二维是每一行的token。在继续讲词法分析器之前,先看看Tinymoe包含多少token:

  • 符号:(、)、,、:、&、+、-、*、/、\、%、<、>、<=、>=、=、<>
  • 关键字:module、using、phrase、sentence、block、symbol、type、cps、category、expression、argument、assignable、list、end、and、or、not
  • 数字:123、456.789
  • 字符串:"abc\r\ndef"
  • 标识符:tinymoe
  • 注释:-- this is a comment

 

Tinymoe对于token有一个特殊的规定,就是字符串和注释都是单行的。因此如果一个字符串在没有结束之前就遇到了换行,那么这种写法定义为你遇到了一个没有右双引号的字符串,需要报个错,然后下一行就不是这个字符串的内容了。

 

一个词法分析器所需要做的事情,就是把一个字符串分解成描述此法的数据结构。既然上面已经说到Tinymoe的token列表是二维的,因此数据结构肯定会体现这个观点。Tinymoe的词法分析器代码可以在这里找到:https://github.com/vczh/tinymoe/blob/master/Development/Source/Compiler/TinymoeLexicalAnalyzer.h

 

首先是token:

CodeTokenType是一个枚举类型,标记一个token的类型。这个类型比较细化,每一个关键字有自己的类型,每一个符号也有自己的类型,剩下的按种类来分。我们可以看到token需要记录的最关键的东西只有三个:类型、内容和代码位置。在token记录代码位置是十分重要的,正确地记录代码位置可以让你能够报告带位置的错误、从语法树的节点还原成代码位置、甚至在调试的时候可以把指令也换成位置。

 

这里需要提到的是,string_t是一个typedef,具体的声明可以在这里看到:https://github.com/vczh/tinymoe/blob/master/Development/Source/TinymoeSTL.h。Tinymoe是完全由标准的C++11和STL写成的,但是为了适应不同情况的需要,Tinymoe分为依赖code page的版本和Unicode版本。如果编译Tinymoe代码的时候声明了全局的宏UNICODE_TINYMOE的话,那Tinymoe所有的字符处理将使用wchar_t,否则使用char。char的类型和Tinymoe编译器在运行的时候操作系统当前的code page是绑定的。所以这里会有类似string_t啊、ifstream_t啊、char_t等类型,会在不同的编译选项的影响下指向不同的STL类型或者原生的C++类型。github上的VC++2013工程使用的是wchar_t的版本,所以string_t就是std::wstring。

 

Tinymoe的词法分析器除了token的类型以外,肯定还需要定义整个文件结构在词法分析后的结果:

这个数据结构体现了"Tinymoe的token列表是二维的"的这个观点。一个文件会被词法分析器处理成一个shared_ptr<CodeFIle>对象,CodeFile::lines记录了所有非空的行,CodeLine::tokens记录了该行的所有token。

 

现在让我们来看词法分析的具体过程。关于如何从正则表达式构造词法分析器可以在这里(http://www.cppblog.com/vczh/archive/2008/05/22/50763.html)看到,不过我们今天要讲一讲如何人肉构造词法分析器。方法其实是一样的,首先人肉构造状态机,然后把用状态机分析输入的字符串的代码抄过来就可以了。但是很少有人会解耦得那么开,因为这样写很容易看不懂,其次有可能会遇到一些极端情况是你无法用纯粹的正则表达式来分词的,譬如说C++的raw string literal:R"tinymoe(这里的字符串没有转义)tinymoe"。一个用【R"<一些字符>(】开始的字符串只能由【)<同样的字符>"】来结束,要顺利分析这种情况,只能通过在状态机里面做hack才能解决。这就是为什么我们人肉构造词法分析器的时候,会把状态和动作都混在一起写,因为这样便于处理特殊情况。

 

不过幸好的是,Tinymoe并没有这种情况发生。所以我们可以直接从状态机入手。为了简单起见,我在下面的状态机中去掉所有不是+和-的符号。首先,我们需要一个起始状态和一个结束状态:

 

首先我们添加整数和标识符进去:

 

其次是加减和浮点:

 

最后把字符串和注释补全:

 

这样状态机就已经完成了。读过编译原理的可能会问,为什么终结状态都是由虚线而不是带有输入的实现指向的?因为虚线在这里有特殊的意义,也就是说它不能移动输入的字符串的指针,而且他还要负责添加一个token。当状态跳到End之后,那他就会变成Start,所以实际上Start和End是同一个状态。这个状态机也不是输入什么字符都能跳转到下一个状态的。所以当你发现输入的字符让你无路可走的时候,你就是遇到了一个词法错误

 

这样我们的设计就算完成了,接下来就是如何用C++来实现它了。为了让代码更容易阅读,我们应该给Start和1-9这么多状态起名字,做法如下:

在这里类似状态3这样的状态被我省略掉了,因为这个状态唯一的出路就是虚线,所以跳到这个状态意味着你要立刻执行虚线,也就是说你不需要做"跳到这个状态"这个动作。因此它不需要有一个名字。

 

然后你只要按照下面的做法翻译这个状态机就可以了:

 

只要写到这里,那么我们就初步完成了词法分析器了。其实任何系统的主要功能都是相对容易实现的,往往是次要的功能才需要花费大量的精力来完成,而且还很容易出错。在这里"次要的功能"就是——记录token的行列号,还有维护CodeFile::lines避免空行的出现!

 

尽管我已经做过了那么多次词法分析器,但是我仍然无法一气呵成写对,仍然会出一些bug。面对编译器这种纯计算程序,debug的最好方法就是写单元测试。不过对于不熟悉单元测试的人来讲可能很难一下子想到要做什么测试,在这里我可以把我给Tinymoe谢的一部分单元测试在这里贴一下。

 

第一个,当然是传说中的"Hello, world!"测试了:

 

TEST_CASE和TEST_ASSERT(这里暂时没有直接用到TEST_ASSERT)是我为了开发Tinymoe随手撸的一个宏,可以在Tinymoe的代码里看到。为了检查我们有没有粗心大意,我们有必要检查词法分析器的任何一个输出的数据,譬如每一行有多少token,譬如每一个token的行号列好内容和类型。我为了让这些枯燥的测试用例容易看懂,在这个文件(https://github.com/vczh/tinymoe/blob/master/Development/TinymoeUnitTest/TestLexicalAnalyzer.cpp)的开头可以看到FIRST_LINE、FIRST_TOKEN、TOKEN、LAST_TOKEN、NEXT_LINE、LAST_LINE是怎么定义的。其实吧这些宏展开,就是一个普通的遍历CodeFile::lines和CodeLine::tokens的程序,然后TEST_ASSERT一下CodeToken的每一个成员是否我们所需要的值。我相信看到这里很多人肯定把重点放在宏和炫技上,而不是如何设计测试用例上,这是有前途的程序员和没前途的程序员面对一份资料的反应的重要区别之一。没前途的程序员总是会把注意力放在一些莫名其妙的地方,其中一个例子就是"过早优化"。

 

第二个测试用例针对的是整数和浮点的输出和报错上,重点在于检查每一个token的列号是不是正确的计算了出来:

 

第三个测试用例的重点主要是-符号和—注释的分析:

 

第四个测试用例则是测试字符串的escaping和在面对换行的时候是否正确的处理(之前提到字符串不能换行,遇到一个突然的换行将会被看成缺少双引号):

 

鉴于词法分析本来内容也不多,所以这篇文章也不会太长。相信有前途的程序员也会在这里得到一些编译原理以外的知识。下一篇文章将会描述Tinymoe的函数头的语法分析部分,将会描述一个编译器的不带歧义的语法分析是如何人肉出来的。敬请期待。

跟vczh看实例学编译原理——二:实现Tinymoe的词法分析的更多相关文章

  1. 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...

  2. 跟vczh看实例学编译原理——一:Tinymoe的设计哲学

    自从<序>胡扯了快一个月之后,终于迎来了正片.之所以系列文章叫<看实例学编译原理>,是因为整个系列会通过带大家一步一步实现Tinymoe的过程,来介绍编译原理的一些知识点. 但 ...

  3. 跟vczh看实例学编译原理——零:序言

    在<如何设计一门语言>里面,我讲了一些语言方面的东西,还有痛快的喷了一些XX粉什么的.不过单纯讲这个也是很无聊的,所以我开了这个<跟vczh看实例学编译原理>系列,意在科普一些 ...

  4. 看动画学算法之:二叉搜索树BST

    目录 简介 BST的基本性质 BST的构建 BST的搜索 BST的插入 BST的删除 简介 树是类似于链表的数据结构,和链表的线性结构不同的是,树是具有层次结构的非线性的数据结构. 树是由很多个节点组 ...

  5. [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

    本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...

  6. [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST

    本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...

  7. 学了编译原理能否用 Java 写一个编译器或解释器?

    16 个回答 默认排序​ RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和 ...

  8. 编译原理——求解First,Follow,Firstvt和Lastvt集合

    转载地址 http://dongtq2010.blog.163.com/blog/static/1750224812011520113332714/ 学编译原理的时候,印象最深的莫过于这四个集合了,而 ...

  9. 编译原理LR(0)项目集规范族的构造详解

    转载于https://blog.csdn.net/johan_joe_king/article/details/79051993#comments 学编译原理的时候,感觉什么LL(1).LR(0).S ...

随机推荐

  1. gcc -Wall -pedantic -ansi(转载)

    转载自R-G-Y-CQ的新浪博客 -Wall显示所有的警告信息 -Wall选项可以打开所有类型的语法警告,以便于确定程序源代码是否是正确的,并且尽可能实现可移植性. 对Linux开发人员来讲,GCC给 ...

  2. POJ 1743 Musical Theme ——后缀数组

    [题目分析] 其实找最长的不重叠字串是很容易的,后缀数组+二分可以在nlogn的时间内解决. 但是转调是个棘手的事情. 其实只需要o(* ̄▽ ̄*)ブ差分就可以了. 背板题. [代码] #include ...

  3. ubuntu14.04禁用自动待机保持屏幕亮度

    http://jingyan.baidu.com/article/9989c7461fd041f648ecfe05.html

  4. 用Fiddler的自动响应模拟系统集成

    1. 下载最新版本的Fiddler Fiddler 官网 2, 安装并启动Fiddler 3, 勾选自动响应 见上图 4, 添加自动响应规则 见上图 5, 添加自动响应内容文件 添加响应文件到Fidd ...

  5. iTunes使用总结

    UDID查询 将设备连接至电脑,打开iTunes至设备摘要页面,鼠标点击"序列号"区域切换显示UDID

  6. 【转】iOS学习之适配iOS10

    适配iOS10 2016年9月7日,苹果发布iOS 10.2016年9月14日,全新的操作系统iOS 10将正式上线. 作为开发者,如何适配iOS10呢? 1.Notification(通知) 自从N ...

  7. C到C++的升级

    const 在C中只是个“只读变量”,并不是真正意义上的常量,通过指针能改变它,如下 #include<stdio.h> int main() { ;//声明只读变量a为0 int* p= ...

  8. weex scroller

    今天学习了一下weex的 scroller.就简单地对其整理一下自己的学习笔记. <scroller>这个标签只能出现在列(column)上面, 只有当它自己的内容大于类似与PC父级的高度 ...

  9. smack 4.1创建群聊

    smack 4.1.1版本对群聊修改了很多,MultUserChat的构造函数修改成了私有,以前通过new MultUserChat创建聊天室,现在通过MultUserChatMananger先通过r ...

  10. 复习排序with javascript

    最近又翻看了一下数据结构(数据结构学渣). 以前总是看不懂,连冒泡和选择排序都要纠结半天,后来才慢慢有意识能区分开来. 当真的理解了快速排序之后,才觉得,这是个很赞的排序,很容易理解. 于是简单的,模 ...