以下是「第2章 词法分析器」的读书笔记。
关键词:有限自动机、正则表达式、不动点。
识别器
这一节用「状态转移图」来描述识别器。
识别单词
假设要识别 new 关键字,那么识别器中可能存在这样的代码:
c <- NextChar()
if c = 'n'
c <- NextChar()
if c = 'e'
c <- NextChar()
if c = 'w'
return 'success'
else
throw 'error'
end
else
throw 'error'
end
else
throw 'error'
end
我们可以用 转移图
来表示这一逻辑:
图中的 S0、S1、S2 和 S3 表示计算过程中的一个抽象状态,S0 一般用于表示起始状态,有两层圆圈的 S3 表示接受状态。
那么识别 while
的转移图就是这样的:
识别 not
的转移图是这样:
因此,识别 new
not
和 while
的转移图就是这样的:
识别器的形式化
我们可以把识别 new
not
和 while
的有限自动机形式化为:
其中
- 表示所有状态,其中 是错误状态,转移图一般不画 。
- 是识别器使用的字母表
- 是识别器的所有转移函数
- 是初始状态
- 是接受状态的集合
任何形式上满足这 5 个条件( 、 、 、 、 )的数学对象都叫做「有限状态机」,英文缩写为 FA。
识别数字
这个图有两个问题:
- 它无法终结,这违反了 是有限集的定义。
- 从 开始,所有的状态都是等价的,因为他们期待同样的数字切均为接受状态。
因此我们可以允许 FA 有环,这可以大大简化 FA:
识别标识符
不唯一
一个识别器对应的状态转移图很可能不唯一。
正则表达式
识别器除了用「状态转移图」来描述,还可以用「正则表达式」来描述。
但什么是正则表达式呢?
形式化定义
一个正则表达式由三个基本操作构建而成:
-
选择,语法为 ,定义为
-
连接,语法为 ,定义为
-
闭包,语法为 ,定义为
- 正闭包,语法为 ,其本质是
举例,用上面定义的正则表达式来表示一个识别标识符的识别器是这样的:
[a-zA-Z]([a-zA-Z0-9])*
可以用正则表达式定义的语言被称为「正则语言」。
从正则表达式到确定性有限状态机
这一小节一开始就给出了一个结论:从有限状态机可以自动推导出正则表达式,反之亦然。其中的构造法如下:
为了理解这些构造法,我们需要将 FA 区分为 DFA(确定性有限状态机)和 NFA(非确定性有限状态机)。
- NFA 允许在 (空字符串)上进行转移。
- DFA 不允许。
我个人感觉这部分内容过于学术,没有必要理解。直接记住下面结论即可:
- Thompson 构造法可以把正则表达式变成 NFA
- 子集构造法可以把 NFA 变成 DFA
- Hopcroft 算法可以把 DFA 简化成最小 DFA(最小是指状态数最小)
- 推论:通过上面三个算法,我们可以把正则表达式变成最小 DFA
- DFA 变成识别器代码是很容易的事情(下节讲)
- 推论:只要用正则描述一门语言,就可以很容易的得到识别器的代码
从 DFA 到代码
将 DFA 转换为可执行的代码有三种策略:
- 表驱动
- 直接编码
- 手工编码
三种策略都是在模拟状态机的运转方式:不停地读取输入字符串中的下一个字符,并模拟状态转移。它们的不同之处在于对 DFA 转移结构的建模方式和模拟 DFA 操作的方式。
表驱动
假设某编程语言的标识符的正则表达式为 $[0-9]+
,那么对应的 DFA 可能为:
我们需要把字符分类,列成一个表,我们将此表命名为「字符分类表」:
$ | 0-9 | EOF | 其他 |
---|---|---|---|
prefix | number | other | other |
然后我们可以把所有的状态转移列成一个表,我们将此表命名为「转移表」:
prefix | number | other | |
---|---|---|---|
其中只有 是接受状态。
我们还可以将每种状态是否属于接受状态进行列表,此表名为「结果类型表」
无效 | 无效 | 标识符 | 无效 |
那么代码就很好写了:
function nextWord(){
let state = 's_0'
let lexeme = ''
let stack = []
stack.push('bad')
while (state !== 's_error') {
nextChar(char) // 此函数以后介绍
lexeme += char
if(['s_2'].include(state)){
stack = []
}
stack.push(state)
categoty = 字符分类表[char]
state = 转移表[state][category]
}
// 如果最终 state 不是接受状态,则回滚到之前的接受状态(如果有)
while (!['s_2'].include(state) && state !== 'bad'){
state = stack.pop()
truncate(lexeme)
rollback() // 此函数以后介绍
}
if(['s_2'].include(state)){
return 结果类型表[state]
} else {
return '无效'
}
}
未完待续……