2. 手工构建词法分析器:状态转移图实现,关键字与标识符识别
好,咱们今天来聊聊词法分析器的手工构建。说实话,很多教材一上来就讲 Lex 或 Flex 这种自动生成工具,确实方便。但我个人觉得,如果你没亲手画过状态转移图,没手写过识别逻辑,你对词法分析的理解始终隔着一层纱。
我当年刚入行时,就踩过这个坑。项目里用工具生成了一个词法分析器,结果遇到一个诡异的 bug——关键字被识别成了标识符。查了半天,才发现是工具生成的 DFA 状态合并出了问题。从那以后,我坚持手工构建核心逻辑。你想想看,自己画的状态图,自己写的代码,哪里容易出问题心里门儿清。
2.1 状态转移图:词法分析器的骨架
状态转移图,说白了就是一张“地图”。它告诉你:当前在哪个状态,读入一个字符后,下一步该去哪。
举个例子,我们要识别一个简单的标识符(比如变量名)。规则是:以字母或下划线开头,后面跟字母、数字或下划线。对应的状态图长这样:
状态0(起始) --字母/下划线--> 状态1(标识符中)
状态1(标识符中) --字母/数字/下划线--> 状态1(标识符中)
状态1(标识符中) --其他字符--> 状态2(接受/结束)
嗯,这里要注意:状态2其实是一个“伪状态”,它表示我们已经读到了一个完整的标识符,需要把当前字符回退回去,交给下一轮分析。
我建议你画图时,用圆圈表示状态,用箭头表示转移,箭头上标注触发字符。这样一目了然。我在项目中习惯用 Graphviz 画,但手绘在白板上也完全够用。
2.2 关键字与标识符:一对“双胞胎”
关键字和标识符长得太像了。比如 if,它既可以是关键字,也可以是变量名(虽然大多数语言禁止)。怎么区分?
核心思路就一句话:先识别,后查表。
具体来说:
- 用同一套状态图识别出“单词”
- 识别完成后,去关键字哈希表里查一下
- 如果在表中,就是关键字;否则就是标识符
我曾经见过有人试图在状态图里直接区分关键字,比如为 if 单独画一条路径。结果关键字一多,状态图变成了一团乱麻。千万别这么干。
2.3 手工实现:从状态图到代码
好,理论说完了,咱们直接上代码。下面是一个极简的词法分析器核心逻辑,只处理标识符和关键字。
// 伪代码,但逻辑完整
int state = 0;
char ch = nextChar();
StringBuilder token = new StringBuilder();
while (ch != EOF) {
switch (state) {
case 0: // 起始状态
if (isLetter(ch) || ch == '_') {
token.append(ch);
state = 1;
ch = nextChar();
} else {
// 处理其他 token(数字、运算符等)
}
break;
case 1: // 标识符中
if (isLetterOrDigit(ch) || ch == '_') {
token.append(ch);
ch = nextChar();
} else {
// 识别完成,查关键字表
String word = token.toString();
if (keywordSet.contains(word)) {
return new Token(TokenType.KEYWORD, word);
} else {
return new Token(TokenType.IDENTIFIER, word);
}
// 注意:ch 要回退
state = 0;
token = new StringBuilder();
}
break;
}
}
这段代码看着简单,但有几个坑:
- 字符回退:识别完一个 token 后,当前字符不属于这个 token,必须回退。我习惯用一个
unreadChar()函数处理。 - 空串检查:如果 token 是空串,说明状态图有 bug,直接报错。
- 边界情况:比如文件末尾刚好是一个标识符,要能正确处理。
2.4 状态转移表:另一种实现方式
除了用 switch-case,你还可以用状态转移表。说白了,就是一张二维数组:行是当前状态,列是输入字符类别,值是下一个状态。
| 当前状态 | 字母/下划线 | 数字 | 其他 |
|---|---|---|---|
| 0(起始) | 1 | 错误 | 错误 |
| 1(标识符中) | 1 | 1 | 接受 |
用表的好处是:逻辑统一,容易扩展。加一个新 token 类型,只需要加几行表项。坏处是:表大了以后,维护起来有点费劲。
我个人习惯:小项目用 switch-case,大项目用状态转移表。没有绝对的好坏,看场景。
2.5 实战建议:从零开始搭一个
如果你真想学会手工构建词法分析器,我建议你按这个步骤来:
- 先画图:在白板上画出所有 token 的状态转移图。别急着写代码。
- 走查一遍:用手模拟几个输入串,看看状态怎么跳转。比如输入
if123,看看能不能正确识别为标识符。 - 写核心逻辑:只写标识符和关键字的识别,其他 token 先放一放。
- 加测试:写几个测试用例,包括正常情况、边界情况、错误情况。
- 逐步扩展:加上数字、运算符、分隔符等。
核心要点总结:
- 状态转移图是词法分析器的骨架,一定要先画清楚
- 关键字和标识符用同一套逻辑识别,最后查表区分
- 字符回退是常见 bug 来源,务必小心
- switch-case 和状态转移表各有优劣,按需选择
嗯,这一章的内容就这些。手工构建词法分析器,说白了就是“画图 + 查表 + 回退”这三板斧。你动手试试,遇到问题再回来翻翻这一章,应该能解决大部分疑惑。