词法分析(2):万法归一的正则文法

28 阅读6分钟

本文是本人撰写的编译原理讲义,本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人。

上一期,经过纯逻辑的推导,我们从简单的实例中总结出了一套描述单词结构的规则。

通过这套规则,我们把标识符和常数(为了方便,下面只讨论十进制无符号整数)这两个看似风马牛不相及的两类单词,用相同的方法描述了出来。

a(标识符):A->[a-zA-Z_]B | [a-zA-Z_];B->[a-zA-Z_0-9]B | [a-zA-Z_0-9] b(十进制无符号整数):C->[0-9]C | [0-9]

聪明的你可能会开始吐槽了:之前不是说一类单词就设计一个处理函数的方法是有问题的,但现在好像依然是一类单词用一套处理方法,那不是一样的吗?不要因为你用了公式化就显得多牛逼一样啊。

好吐槽。如果把一套规则看作是一个函数,那么确实是和之前相比,没有变化。但还记得gemini生成的那段伪代码吗?在一个if中,同时识别了这两类单词。

获取 单词的 第一个字符
if 第一个字符 不是 数字:
    // 可能是标识符,检查一下后面
    for 第二个到最后一个字符:
		if 这个字符 不是 (字母 or 数字 or 下划线):
		   return "无效标识符 (含非法字符)"
		// if循环检查完毕都没问题
		return "标识符"
else 
	if 第一个字符 不是 数字:
		// 可能是十进制常数,检查一下
		for 单词中的每一个字符:
			if 这个字符 不是 数字:
				return "无效单词 (数字开头却有非数字)"
		// 如果循环检查完毕都没问题
		return "十进制无符号常数"
	else
		return "无效标识符 (含非法字符)"

所以,能否一步到位,用一套规则来同时识别这两类单词呢?

既然我们前面已经把下一步该生成什么符号,和它对应的状态进行了分离,那何不直接再做些更刺激的事:让状态跳转,但是不一定生成对应的符号?

这意味着,我们可以假定在脑海里面临着一个二选一的分岔路口:一边是标识符,一边是常数。既然我们早就在规则用上了“|”来达成分支路径的选择,那何不再和新的想法结合,产生新的火花?

S->A | C

A->[a-zA-Z_]B | [a-zA-Z_]
B->[a-zA-Z_0-9]B | [a-zA-Z_0-9] 

C->[0-9]C | [0-9]

恭喜你,掌握了统一的关键技巧:求同存异。

然后聪明的你可能又会提问:原本一类单词用一类规则来处理,到了结束的时候生成的就是这一类单词,这个好理解;那现在两类单词合并在一起,那结束的时候我怎么知道到底生成的具体是什么东西呢?

仔细看,从第一条规则开始,当你选定了其中一条路,那你就不可能和另外一条路有交集。比如如果选了A,那你就不可能进入C状态。所以,我们可以很确定,在A结束的时候生成的一定是标识符;C结束的时候一定是常数。

那我们可以稍稍修改一下这个规则,在结束的时候进入一个特殊的状态。进入了这个状态后,不生成符号,只返回相应的单词类型。

S->A | C
A->[a-zA-Z_]B | [a-zA-Z_]T;B->[a-zA-Z_0-9]B | [a-zA-Z_0-9]T 
C->[0-9]C | [0-9]F
T->{return "标识符"}
F->{return "常数"}

在引入了这种新的特殊状态后,我们实现了在一套规则下返回多类单词的效果。

恭喜你,掌握了做人的关键技巧:有头有尾。

有位喜欢穿别人睡衣的人讲得好:既然要追求刺激,那就要贯彻到底。

对于我们上期总结出来的,在代码中会出现的七类单词的其他五类:运算符,界限符,关键词,不可见字符,注释,可不可以同样用这套规则进行描述呢?

运算符,界限符,关键词,不可见字符都是一些常字符串,他们和十六进制的0x开头是差不多的,所以可以很容易写出来对应的规则。

假定要识别的符号串为abc,那么对应的规则集可以写成:

S->aA; A->bB; B->c;

很繁琐对吗?那或者干脆别一颗一颗拉了,一次拉完:S->abc;

(对于注释,留给读者自行撰写相关规则集。)

这下,全部的碎片都集齐了,我们得到了一套可以同时识别七类单词的规则。

是时候见识万法归一的威力了:

// S 是“总调度室”,一切从这里开始
S -> A | C | K
// --- 标识符分支 --- 
A -> [a-zA-Z_] B 
B -> [a-zA-Z_0-9] B | T_IDENTIFIER
// --- 无符号整数分支 --- 
C -> [0-9] C'
C' -> [0-9] C' | T_INTEGER
// --- 关键字, 还可以再次分支 --- 
K ->  for T_KEYFOR | if T_KEYIF | if B_TEMP |  for B_TEMP....  // 如果符号串“if”/“for ”后接了其他符号,如iframe就不是关键字而是标识符
// --- 独一无二的“终点站” --- 
B_TEMP -> [a-zA-Z_0-9] B
T_IDENTIFIER   -> { if(CheckNotKeyWord()) return "标识符"; else return KeyWord();}  //检查已输入的内容,如果不是关键字,则返回"标识符"。如果是,则返回对应的关键字类型。
T_INTEGER      -> { return "无符号整数" } 
T_KEYIF   -> { return "关键字_IF" }
T_KEYFOR   -> { return "关键字_for" }
....

现在,如果又要你识别一种新的单词,只需要:

1。用上面这套词法规则描述这类新单词;

2。给它添加一个独特的返回值;

3。在规则集中插入新的规则,对原有的规则几乎可以做到互不相干;或者

4。直接更新原有的规则,比如把常数的规则换成带下划线常数的规则。

再也不用在意大利面条一样的屎山中沉浮了。

恭喜你,从只知道特事特办的牛马程序员,朝着成为一名构筑蓝图的架构师迈出了坚实的一步。

也恭喜你,独立推导出了大名鼎鼎的正则文法。

至此,词法分析程序的大厦已经落成,上面只剩下一朵乌云:如何编码这些规则?

下一期,我们将继续使用名为“抽象”的高射炮,对着这朵乌云狠狠开炮,直到阳光照耀大地。