本文是本人撰写的编译原理讲义,本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人。
上一期,经过纯逻辑的推导,我们从简单的实例中总结出了一套描述单词结构的规则。
通过这套规则,我们把标识符和常数(为了方便,下面只讨论十进制无符号整数)这两个看似风马牛不相及的两类单词,用相同的方法描述了出来。
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。直接更新原有的规则,比如把常数的规则换成带下划线常数的规则。
再也不用在意大利面条一样的屎山中沉浮了。
恭喜你,从只知道特事特办的牛马程序员,朝着成为一名构筑蓝图的架构师迈出了坚实的一步。
也恭喜你,独立推导出了大名鼎鼎的正则文法。
至此,词法分析程序的大厦已经落成,上面只剩下一朵乌云:如何编码这些规则?
下一期,我们将继续使用名为“抽象”的高射炮,对着这朵乌云狠狠开炮,直到阳光照耀大地。