【小白打造编译器系列2】实现词法分析器(C++)

通过前文【小白打造编译器系列】编译器的前端技术是什么?我们已经对整个编译器的前端有了一定的了解,那么我们接下来实战一下,实现一个简单的词法分析器。

实现词法分析器,就是写出正则表达式,画出有限自动机的图形,然后根据图形直观地写出解析代码的过程。

我们实现的词法分析器,包括:

  • 关系表达式:age >= 45
  • 变量声明:int age = 40
  • 算术表达式:2*3+5

解析 age >= 45

因为这里实现的是简单版的词法分析器,我们减少一些需求。

定义标识符、比较操作符和数字字面量 Token 的词法规则:

  • 标识符:第一个字符必须是字母,后面的字符可以是字母或数字。
  • 比较操作符:> 和 >=(其他比较操作符暂时忽略)。
  • 数字字面量:全部由数字构成(像带小数点的浮点数,暂时不考虑)。

 我们就根据上面的规则,构建一个有限自动机。有限自动机分为被细分为 5 种状态。

  • 初始状态:刚开始启动词法分析的时候,程序所处的状态。
  • 标识符状态:在初始状态时,当第一个字符是字母的时候,迁移到状态 2。当后续字符是字母和数字时,保留在状态 2。如果不是,就离开状态 2,写下该 Token,回到初始状态。
  • 大于操作符(GT):在初始状态时,当第一个字符是 > 时,进入这个状态。它是比较操作符的一种情况。
  • 大于等于操作符(GE):如果状态 3 的下一个字符是 =,就进入状态 4,变成 >=。它也是比较操作符的一种情况。
  • 数字字面量:在初始状态时,下一个字符是数字,进入这个状态。如果后续仍是数字,就保持在状态 5。

状态转移图如下:

举个例子:一开始我们处于初始状态。当识别出第一个字符是字母的时候就从初始状态1转移到状态2,会继续扫描下一个字符,如果是数字或者字母,就继续保持在状态2,否则将整个读入的字符串作为ID存储起来,然后返回初始状态1,继续如此反复。

定义 Token 和 状态 STATE(完整代码见文末)

  • Token 结构体,包括了 Type(类型)和 Text(文本)。
//创建Token结构体
typedef struct {
	//状态
	STATE state = INIT;
	//内容
	string text = "";
}Token;
  • STATE 状态,使用枚举定义。
//5个状态,使用enum表示
enum STATE
{
	INIT = 0,//初始状态
	ID,//标识符状态
	DIGIT,//数字字面量状态
	GE,//大于等于
	GT //大于
};

代码实现 Token 的状态初始化过程

要记住一点:进入新状态前,要对上一个状态进行收尾工作!!

因此,这里的函数要做的就是把上一个状态的 Token 存起来,然后开启一个新的 Token。

  1. 如果现在的 token 的 text 中有值,说明上一个状态已经结束(我们把拼接字符串的操作在switch中完成),所以我们对上一个状态所记录的 Token 可以直接保存。这一步实际上就是对上一个状态的收尾
  2. 接下来,开启一个新的 Token,对它初始化。
  3. 下一步我们可以进行判断了,根据我们上面的有限自动机转移图。
    1. ch 是字母,则转为 ID 状态
    2. ch 是数字,则转为 DIGIT 状态
    3. ch 是 > ,则转为 GT
  4. 最后需要返回当前 ch 字符所表示的状态,作为上一个状态用于下一次判断(有点拗口)。
//内联函数,解决频繁大量的使用就会造成因栈空间不足而导致程序出错的问题
//状态转移过程
inline STATE MyTokenReader::Init_Token(char ch) {

	//如果有token,则直接放入结果
	if (token.text.length() > 0) {
		vecResult.push_back(token);
		token = Token(); //清0,重新记录
	}
	//设置最初为INIT状态
	token.state = INIT;

	//如果是字母,则状态转为ID
	if (isalpha(ch)) {
		token.state = ID;
		token.text += ch;
	}
	//如果是数字,则状态转为DIGIT
	else if (isdigit(ch)) {
		token.state = DIGIT;
		token.text += ch;
	}
	//如果是>,则状态转为
	else if(ch == '>'){
		token.state = GT;
		token.text += ch;
	}
	return token.state;
}

代码实现 Token 的状态转移过程

状态转移的前提是我们需要知道前一个状态是什么,才能根据现在的到的字符来判断转移到哪一个状态。

于是这里需要记录上一个状态,并且根据上一个状态进行分类讨论。

  1. 前状态为 INIT。说明需要将 Token 初始化(进入新状态)。
  2. 前状态为 ID。若当前 ch 是数字或者是字符,可以继续加入到 Text 中,否则需要进入新状态。
  3. 前状态为 DIGIT。若当前 ch 是数字,可以继续加入 Text 中,否则需要进入新状态。
  4. 前状态是 > 。若当前 ch 是 = ,需要转移状态为 GE,并且加入 Text 中,否则需要进入新状态。
  5. 前状态是 >= 。说明需要将 Token 初始化(进入新状态)。
//解析语句函数
void MyTokenReader::parse() {
	//初始状态为INIT
	STATE lastState = INIT;
	for (size_t i = 0; i < str.length(); i++) {
		//语句从头开始遍历
		char ch = str[i];
		//根据状态来讨论
		switch (lastState) //上一个状态
		{
		case INIT:
			lastState = Init_Token(ch);
			break;
		case ID:
			if (isalpha(ch) || isdigit(ch)) {
				token.text += ch;
			}
			else {
				lastState = Init_Token(ch);
			}
			break;
		case DIGIT:
			if (isdigit(ch)) {
				token.text += ch;
			}
			else {
				lastState = Init_Token(ch);
			}
			break;
		case GT:
			if (ch == '=') {
				token.state = GE;
				token.text += ch;
				lastState = GE;
			}
			else {
				lastState = Init_Token(ch);
			}
			break;
		case GE:
			lastState = Init_Token(ch);
			break;
		default:
			break;
		}
	}
	Init_Token(' ');
}

输出结果

输出结果可以看出,我们对 age、>= 和 45 都做了比较准确的分类。

这正是我们想要的输出结果。我们依据构造好的有限自动机,在不同的状态中迁移,从而解析出 Token 来,只要我们把有限自动机的状态讨论清楚,增加详细里面的状态和迁移路线,就能实现一个完整的词法分类器了。

更准确简单的匹配:正则表达式

当然,如果我们一个一个手写规则肯定工作量很大,同时也没有那么严谨。

对于规则的匹配,我们通常使用一个非常牛逼的表达式——正则表达式。它常被用在各种地方,比如网页前端(或者后端)的输入格式的校验,可以通过字符串判断输入的是不是符合某种格式的;或者一些简单的爬虫上,通过正则表达式取匹配相应的字段内容等。

那么对于上面我们定义的几个状态的正则表达式的定义:

Id :        [a-zA-Z_] ([a-zA-Z_] | [0-9])*
IntLiteral: [0-9]+
GT :        '>'
GE :        '>='

 这里复习一下正则表达式的规则:

当然,在现代编写编译器都是使用正则表达式的,前面只是为了方便解释与理解词法分析器的原理,自己写写,动手总时没有坏处的。

解析 int age = 40(处理标识符和关键字规则冲突)

解析“int age = 40”这个语句,以这个语句为例研究一下词法分析中会遇到的问题:多个规则之间的冲突

如果我们把这个语句涉及的词法规则用正则表达式写出来,是下面这个样子:

Int:        'int'
Id :        [a-zA-Z_] ([a-zA-Z_] | [0-9])*
Assignment : '='

这时候,就发生了一个问题了:int 这个关键字,与标识符很相似,都是以字母开头,后面跟着其他字母。也就是 int 这个字符串同时满足关键字规则标识符规则

这时候怎么处理呢?添加状态呀!

我们当然知道关键字的优先级是高于标识符的,因此当我们读入第一个字符为 i 的时候,就需要讨论了,建立一个新的状态 ID_int1;继续读入一个字符,如果不是 n ,则可以返回ID状态,如果是 n ,那就更要注意了,建立一个新的状态 ID_int2;如果在 ID_int2 后的下一个读入的字符是 t,说明关键字来了,建立新状态 INT。使用图表示如下:

于是,我们很好地避免了规则之间的冲突。

至于 2*3+5 这种计算表达式,我们也很好处理,增加规则即可。

总结

要实现一个词法分析器,首先需要写出每个词法的正则表达式,并画出有限自动机,之后,只要用代码表示这种状态迁移过程就可以了

本文代码:https://github.com/SongJain/TheBeautyOfCompiling/tree/master/BianYiCode


备注:个人原创学习笔记,参考《极客时间-编译原理之美》。

发布了104 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_41960890/article/details/105079831