美文网首页
词法分析(理论篇)

词法分析(理论篇)

作者: wsztrush | 来源:发表于2015-03-23 21:38 被阅读794次

写在前面


从源代码到可执行文件要经历几个过程:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 中间代码生成
  5. 代码优化

词法分析有点像中文分词,是将输入结构化的第一步:从字符串序列变为词法单元序列,它的输出将作为语法分析的输入。在词法单元中主要涉及到的概念有:

  1. DFA:有穷确定状态机。
  2. NFA:有穷不确定状态机。

确定和不确定的区别在于:当遇到字符的时候状态的变化是不是确定的,下面是一个NFA的例子:

NFA

对于这样一个状态机可以构造出一个转换表如下:

状态 a b
1 {1,2} {1}
2 {3}
3

对于DFA来说每个状态上的转换都是确定的,那么其对应的代码的样子如下:

s = s0;
c = nextChar();
while(c != eof){
    s = move(s, c); // 根据状态转换表
    c = nextChar();
}
return s in F ? "yes" : "no";

从代码上来看DFA要比NFA简单不少,那么下面接着来看。

从NFA到DFA


既然DFA看起来要比NFA用起来简单很多,那么如果能将NFA转换为DFA的话问题不就变简单了?

子集构造法是一种比较简单、直观的转化方法,既然在NFA中状态1根据字符a可以到状态1,也可以到状态2,那么如果将状态{1,2}合并看做一个节点,那得到结果不就是DFA了~

从NFA到DFA

在实际操作的时候需要用到三个集合:

  1. ε-closure(s):从NFA中状态s开始只通过ε得到的状态的集合。
  2. ε-closure(T):从T中的某个状态s开始只通过ε得到的状态的集合。
  3. move(T, a):从T中某个状态s开始通过a得到的状态集合。

在NFA上构造完成三个集合之后,那么可以通过如下流程进行转换:

// 在最开始Dstatus中只有ε-closure(s0)一种状态,并未未加标记
while(在Dstatus中有一个未标记的状态T){
    给T加上标记;
    for(每个输入符号a) {
        U = ε-closure(move(T, a));
        if(U 不在Dstates中)
            将U加入到Dstatus中// 不加标记
        Dtran[T, a] = U;// 转换表
    }
}

在实际上DFA和NFA的状态数都是差不多的,但是理论上转换后的DFA可能是原本的NFA状态数的指数级,(a|b)*a(a|b)^(n-1)就是这样一个例子:

DFA状态数为NFA的指数倍

从正则到NFA


很多的词法分析器都是用正则来描述规则,一般正则基本上能搞定所有的需求,而且足够简单。但是在识别输入的字符序列的时候需要用到自动机,那么就需要从正则到NFA or DFA的转换。对于正则的每种行为都有对应的NFA中的转换可以代替:

  1. r=s|t:通过ε实现。
  2. r=st:将s的接受状态和t的初始状态进行合并。
  3. r=s*:将s的接受状态和初始状态相连,从而达到循环的效果。

达到的效果如下:

从正则到NFA

对于(a|b)*a这样的正则按照上面的方法得到的NFA如下:

(a|b)*a对应的NFA

有了这种方法,根据正则的AST就能很容易递归生成NFA。

从正则到DFA


DFA是比较简单的,那么在解析正则的一个思路是正则->NFA->DFA,但是这样做比较麻烦,我们肯定是希望能够直接转换。这里需要四个集合:

  1. nullable(n):节点n的子表达式中包含空字符串。
  2. firstpos(n):节点n为根的子表达式中某个串的第一个符号的位置的集合。
  3. lastpos(n):节点n为根的子表达式中某个串的末一个符号的位置的集合。
  4. followpos(p):如果L((r)#)中的某个串x=a1..an,使得我们在解释为什么x属于L((r)#)时,可以将x中的某个ai和AST中的位置p匹配,且位置ai+1和位置q匹配,那么q在该集合中。

这样看起来比较难理解,来看一个例子:

集合例子

那么对于框中的根节点来说:

  1. nullable(n):false。
  2. firstpos(n):{1,2,3}。
  3. lastpos(n):{3}。
  4. followpos(1):{1,2,3}。

这些集合可以递归得到,比如较为麻烦的followpos的计算方法如下:

  1. 如果n是一个cat节点,其左右子节点分别为c1、c2,那么对于lastpos(c1)中的每个位置i,firstpos(c2)中的所有位置都在followpos(i)中。
  2. 如果n是一个star节点,并且i是lastpos(n)中的一个位置,那么firstpos(n)中所有的位置都在followpos(i)中。

其中关键的followpos其实就是NFA中的状态转换,那么下面的正则到DFA的过程就很容易理解了:

// 构造D的状态集Dstates和D的转换函数Dtran。
初始化Dstates,使之只包含未标记状态firstpos(n0),其中n0是(r)#的抽象语法树的根节点
while(Dstates中存在未标记状态S){
    标记S;
    for(每个输入符号a){
        令U为S中和a对应的所有位置p的followpos(p)的并集;
        if(U不在Dstates中)
            将U作为未标记的状态加入到Dstates中;
        Dtran[S,a] = U;
    }
}

在实际中DFA确实有优势,但是还有进一步优化的空间,接着往下看。

DFA状态最小化


在DFA中有一些状态是的等价的,等价的意思就是没有区别的,那有区别的意义在于:

从状态s、t出发,沿着标号为x的路径到达两个状态,分别为接受状态和非接受状态。

下面是一个简单的最小化的例子:

状态最小化

那么最小化呢?上面的这个定义太抽象了,如果直接找路径的话估计会累死的。想到每条路径都是由字符一点一点拼接成的,那么对应的方法也就比较容易能想到了:

  1. 将所有状态根据接受、非接受分成两组。
  2. 遍历分组、字符,如果同一组内的状态根据该字符到达了不同的组,那么继续将当前的组进行分割。
  3. 重复执行2,直到没有变化。
  4. 每个组中选择一个代表状态,重新构造DFA,最小化完成。

最后给出一个结论:任何一个正则表达式都有唯一的状态数最少的DFA

相关文章

  • 词法分析(理论篇)

    写在前面 从源代码到可执行文件要经历几个过程: 词法分析 语法分析 语义分析 中间代码生成 代码优化 词法分析有点...

  • Javascript 语法解析

    执行过程 词法分析 -> 语法分析 -> 预编译 -> 解释执行 一. 词法分析 核心:词法分析是将字符流(cha...

  • 一个编译器最简前端的python实现

    一个编译器的前端通常包括词法分析器和语法分析器。在分析过程中,文本输入词法分析器,根据词法规则解析出词法单元。词法...

  • PL/0简单编译系统(二)

    词法分析 词法分析又称词法分析器或者扫描器,是编译程序的基本子程序之一。本项目采用手工方式设计并实现词法分析程序。...

  • 编译原理笔记

    源程序分析: 词法分析 :线性分析 被称为词法分析 或者扫描比如:position=init+rate * 60 ...

  • JavaScript引擎、编译器、作用域

    JavaScript理论 编译器理论 传统的编译器一般包括下面的三种基础步骤: (1)分词、词法分析; (2)解析...

  • 编译原理基础知识汇总

    前端: 词法分析 -> 语法分析 -> 语义分析后端: 生成中间代码 -> 优化 -> 生产目标代码 词法分析:有...

  • 三. Flex进阶:需要了解的一些知识

    参考:词法分析器生成工具flex词法分析器总结--flex&bison词法分析生成器flex的选项 1. Flex...

  • 基于LLVM的编译原理简明教程 (2) - 词法与语法分析初步

    递归 - 词法分析与语法分析的分界 一般来说,决定词法分析和语法分析的界限是是否需要递归。词法分析是将输入的符号流...

  • 龙书 第三章

    词法单元:词法分析器扫描源程序并输出一个由词法单元组成的序列。这些词法单元通常会逐个传送给语法分析器。有些词法单元...

网友评论

      本文标题:词法分析(理论篇)

      本文链接:https://www.haomeiwen.com/subject/gqgixttx.html