《编译器设计》笔记2

以下是「第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

我们可以用 转移图 来表示这一逻辑:

image.png

图中的 S0、S1、S2 和 S3 表示计算过程中的一个抽象状态,S0 一般用于表示起始状态,有两层圆圈的 S3 表示接受状态。

那么识别 while 的转移图就是这样的:

image.png

识别 not 的转移图是这样:

image.png

因此,识别 new notwhile 的转移图就是这样的:

image.png

识别器的形式化

我们可以把识别 new notwhile 的有限自动机形式化为:

1b219ab58c3b28d0430d3b1b058acef.jpg

其中

  • S S 表示所有状态,其中 s e s_e 是错误状态,转移图一般不画 s e s_e
  • Σ \Sigma 是识别器使用的字母表
  • δ \delta 是识别器的所有转移函数
  • s 0 s_0 是初始状态
  • S A S_A 是接受状态的集合

任何形式上满足这 5 个条件( S S Σ \Sigma δ \delta s 0 s_0 S A S_A )的数学对象都叫做「有限状态机」,英文缩写为 FA。

识别数字

image.png

这个图有两个问题:

  1. 它无法终结,这违反了 S S 是有限集的定义。
  2. S 2 S_2 开始,所有的状态都是等价的,因为他们期待同样的数字切均为接受状态。

因此我们可以允许 FA 有环,这可以大大简化 FA:

image.png

识别标识符

image.png

不唯一

一个识别器对应的状态转移图很可能不唯一。

正则表达式

识别器除了用「状态转移图」来描述,还可以用「正则表达式」来描述。

但什么是正则表达式呢?

形式化定义

一个正则表达式由三个基本操作构建而成:

  1. 选择,语法为 R S R|S ,定义为 x x R x S {x | x \in R 或 x \in S }

  2. 连接,语法为 R S RS ,定义为 x y x R y S {xy | x \in R 且 y \in S }

  3. 闭包,语法为 R R* ,定义为 ϕ R R R R R R R R R R . . . { \phi | R | RR | RRR | RRRR | ... }

    • 正闭包,语法为 R + R+ ,其本质是 R R RR*

举例,用上面定义的正则表达式来表示一个识别标识符的识别器是这样的:

[a-zA-Z]([a-zA-Z0-9])*

可以用正则表达式定义的语言被称为「正则语言」。

从正则表达式到确定性有限状态机

这一小节一开始就给出了一个结论:从有限状态机可以自动推导出正则表达式,反之亦然。其中的构造法如下:

f91ac82d0cbc01afd8b982043870bb2.jpg

为了理解这些构造法,我们需要将 FA 区分为 DFA(确定性有限状态机)和 NFA(非确定性有限状态机)。

  1. NFA 允许在 ϵ \epsilon (空字符串)上进行转移。
  2. DFA 不允许。

我个人感觉这部分内容过于学术,没有必要理解。直接记住下面结论即可:

  1. Thompson 构造法可以把正则表达式变成 NFA
  2. 子集构造法可以把 NFA 变成 DFA
  3. Hopcroft 算法可以把 DFA 简化成最小 DFA(最小是指状态数最小)
  4. 推论:通过上面三个算法,我们可以把正则表达式变成最小 DFA
  5. DFA 变成识别器代码是很容易的事情(下节讲)
  6. 推论:只要用正则描述一门语言,就可以很容易的得到识别器的代码

从 DFA 到代码

将 DFA 转换为可执行的代码有三种策略:

  1. 表驱动
  2. 直接编码
  3. 手工编码

三种策略都是在模拟状态机的运转方式:不停地读取输入字符串中的下一个字符,并模拟状态转移。它们的不同之处在于对 DFA 转移结构的建模方式和模拟 DFA 操作的方式。

表驱动

假设某编程语言的标识符的正则表达式为 $[0-9]+,那么对应的 DFA 可能为:

image.png

我们需要把字符分类,列成一个表,我们将此表命名为「字符分类表」:

$ 0-9 EOF 其他
prefix number other other

然后我们可以把所有的状态转移列成一个表,我们将此表命名为「转移表」:

prefix number other
s 0 s_0 s 1 s_1 s e r r o r s_{error} s e r r o r s_{error}
s 1 s_1 s e r r o r s_{error} s 2 s_2 s e r r o r s_{error}
s 2 s_2 s e r r o r s_{error} s 2 s_2 s e r r o r s_{error}
s e s_e s e r r o r s_{error} s e r r o r s_{error} s e r r o r s_{error}

其中只有 s 2 s_2 是接受状态。

我们还可以将每种状态是否属于接受状态进行列表,此表名为「结果类型表」

s 0 s_0 s 1 s_1 s 2 s_2 s 3 s_3
无效 无效 标识符 无效

那么代码就很好写了:

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 '无效'
  }
  
}

未完待续……

猜你喜欢

转载自juejin.im/post/7125457288215592968