用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(1)- 目标和前言
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(2)- 简介和设计
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(3)- 词法分析
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(4)- 语法分析1:EBNF和递归下降文法
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(5)- 语法分析2: tryC的语法分析实现
用c语言手搓一个600行的类c语言解释器: 给编程初学者的解释器教程(6)- 语义分析:符号表和变量、函数
项目github地址及源码:
https://github.com/yunwei37/tryC
这一篇讲讲在tryC中词法分析器是怎样构建的
词法分析器是什么玩意
回想一下上一篇我们说的词法分析阶段,编译器做了这样一件事:
对源程序进行阅读,并将字符序列,也就是源代码中一个个符号收集到称作记号(token)的单元中
帮编译器执行词法分析阶段的模块,就叫词法分析器啦。词法分析器能够对源码字符串做预处理,以减少语法分析器的复杂程度。
词法分析器以源码字符串为输入,输出为标记流(token stream),即一连串的标记,比如对于源代码中间:
num = 123.4;
这样一个赋值语句中,变量num算是一个token,“=”符号算是一个token,“123.4”算是一个token;每个token有自己的类别和属性,比如“123.4”的类别是数字,属性(值)是123.4;每个token可以用这一对儿表示:{token, token value},就像“123.4”可以表示为{Num, 123.4}
词法分析器输入上面那句话,就得到这样一个标记流:
{
Sym, num}, {
'=', assign}, {
Num, 123.4}
词法分析器的具体实现
由于词法分析器对于各个语言基本都是大同小异,在其他地方也有很多用途,并且手工构造的话实际上是一个很枯燥又容易出错的活计,因此其实已经有了不少现成的实现,比如 lex/flex 。
通常词法分析器的实现会涉及到正则表达式、状态机的一些相关知识,或者通过正则表达式用上面那些工具来生成。但对于我们这样一个简单的解释器来说,手工构造词法分析器,并且完全不涉及到正则表达式的知识,理解起来也并不是很困难啦。
先来看看token是怎样写的
token的数据结构如下:
int token; // current token type
union tokenValue {
// token value
symbol* ptr;
double val;
} token_val;
- 用一个整型变量 token 来表示当前的 token 是什么类型的;
- 用一个联合体来表示附加的token属性,ptr可以附加指针类型的值,val可以附加数值。
token 的类型采用枚举表示定义:
/* tokens and classes (operators last and in precedence order) */
enum {
Num = 128, Char, Str, Array, Func,
Else, If, Return, While, Print, Puts, Read,
Assign, OR, AND, Equal, Sym, FuncSym, ArraySym, Void,
Nequal, LessEqual, GreatEqual, Inc, Dec
};
比如我们会将“==”标记为Equal,将Num标记为Sym等等。从这里也可以看出,一个标记(token)可能包含多个字符;而词法分析器能减小语法分析复杂度的原因,正是因为它相当于通过一定的编码(采用标记来表示一定的字符串)来压缩和规范化了源码。
另外,一些单个字符我们直接作为token返回,比如:
'}' '{' '(' ')' ';' '[' ']' .....
词法分析器真正干活的函数们
首先需要说明一下,源码字符串为输入,输出为标记流(token stream),这里的标记流并不是一次性将所有的源代码翻译成长长的一串标记串,而是需要一个标记的时候再转换一个标记,原因如下:
- 字符串转换成标记流有时是有状态的,即与代码的上下文是有关系的。
- 保存所有的标记流没有意义且浪费空间。
所以通常的实现是提供一个函数,每次调用该函数则返回下一个标记。这里说的函数就是 next() 。
这是next()的基本框架:其中“AAA”"BBB"是token类型;
void next() {
while (token = *src) {
++src;
if(token == AAA ){
.....
}else if(token == BBB ){
.....
}
}
}
用while循环的原因有以下几个:
-
处理错误:
如果碰到了一个我们不认识的字符,可以指出错误发生的位置,然后用while循环跳过当前错误,获取下一个token并继续编译; -
跳过空白字符;
在我们实现的tryC语言中,空格是用来作为分隔用的,并不作为语法的一部分。因此在实现中我们将它作为“不识别”的字符进行跳过。
现在来看看AAA、BBB具体是怎样判断的:
换行符和空白符
...
if (token == '\n') {
old_src = src; // 记录当前行,并跳过;
}
else if