• 词法分析


    词法分析器的任务是按照一定模式从源程序中识别出记号(token).

    我们使用正规式描述这一模式,并通过有限自动机进行识别.

    正规式与正规集

    语言是在有限字母表上有限长字符串的集合.

    正规式又称正则表达式, 是一种特殊的字符串用来描述一类的字符串的集合.

    我们把可用正规式描述(其结构)的语言称为正规语言或正规集.

    在介绍正规式之前, 我们先定义几个字符串集合中的概念:

    • (epsilon): 空串, 没有字符的串不是空格由空格组成的串

    • 并运算: $ L cup M = {s| s in L or s in M }$

    • 交运算: $ L cap M = {s| s in L and s in M}$

    • 连接运算: $ LM = {st | s in L and t in M } $ , 任意属于L的字符串与任意属于M的字符串按顺序连接

    • 闭包运算: L* $ = L^0 cup L cup L^2 $ ..., 其中(L^0 = {epsilon}, L^2 = LL)

    • 正闭包: $ L+ = L cup L^2... $

    正规式采用递归定义:

    • (epsilon)是正规式, 表示集合({epsilon})

    • 任意字符a是正规式, 表示集合{a}

    • 若r和s是正规式, L(r)和L(s)是它们则:

      • r|s是正规式, 表示的集合为$ L(r) cup L(s)$

      • rs是正规式, 表示的集合为$ L(r)L(s) $

      • r是正规式, 表示的集合为L(r)

      • r+是正规式, 表示的集合为$ L(r)+ $

    上面同样定义了正则表达式的基本运算, 运算均为左结合, 优先级从高到低为闭包, 连接, 交并运算,正规式运算可以使用括号改变顺序.

    两个正规式描述的集合相同则称它们是等价的, 也就是说正规式和集合之间是多对一的关系.

    有限自动机

    不确定的有限自动机(Nondeterministic Finite Automaton, NFA)是识别模式的方法, 我们用状态图来描述NFA.

    下图的NFA用于识别正规式= < | <= | <> | = | > | >=

    NFA的初始状态为0, 若其第一个字符为<则转移到状态1, 其它类推.

    若下一个字符为=则转移到状态2并返回, 若下一个字符为>则转移到状态3并返回, 否则直接返回状态1.这里的返回是指以当前状态作为终态, 终止匹配.

    上面的策略体现了最长匹配原则, 即达到状态1时不立即返回而是继续尝试状态2或状态3.

    NFA识别字符串就是反复试探所有路径,直到到达终态后返回,或者到达不了终态后放弃.一般使用回溯法试探所有路径.

    我们使用五个要素描述NFA:

    • 状态集S

    • 字母表

    • move(i, j)状态转移函数

    • S0初态

    • F终态集

    状态转移函数接受两个参数, 当前状态和转移条件, 返回新的状态.

    转移条件是指后续字符串满足该条件才会发生状态转移, 比如要求下一个字符为特定字符.

    NFA的问题在于:

    • 只有尝试了全部可能的路径,才能确定一个输入序列不被接受

    • 识别过程中需要进行大量回溯,时间复杂度很高

    NFA中对状态转移函数几乎没有限制, 允许其出现一对多的状态转移和(epsilon)状态转移.

    所谓(epsilon)状态转移是指转移条件为空, 状态转移可以随意发生, 这种状态转移经常在识别闭包时出现.

    确定的有限自动机 (Deterministic Finite Automaton, DFA)是NFA的一个特例,其最大的特点是其状态转移函数都是一对一的且不允许(epsilon)状态转移.

    因为NFA对状态转移不加限制在实际应用中带来很多问题, 通常我们将NFA转换为等价的DFA. 这里所谓的自动机等价是指它们识别同样的正规集.

    以正规式(a|b)*abb为例, 其NFA可以表示为:

    可以看到状态为0, 下一个字符为a时出现一对多的问题.

    DFA的状态转移复杂一些:

    词法分析器

    构建词法分析器一般需要几个步骤:

    1. 用正规式描述记号的模式

    2. 为正规式设计NFA

    3. 将NFA转换为等价的DFA, 这一步称为确定化

    4. 优化DFA使其状态数最少, 这一步称为最小化

    从正规式到NFA

    Thompson算法可以用来为一个正规式构建NFA.

    • (epsilon), 构造NFA:

    • 对字符a构造NFA:

    • r和s是正规式, 它们的NFA为N(r)和N(s):

      • r|s的NFA为:

      • rs的NFA为:

      • r*的NFA为:

    使用Thompsonn算法构造正规式(a|b)*abb的NFA, 自下而上构建:

    作出状态转移图:

    从NFA到DFA

    基于NFA构造DFA的核心在于将一对多的状态转移确定化.

    使用回溯法在发现无法匹配的路径后返回是一种自然的思路, 不过我们可以采用并行的方法.

    当发现一对多的情况时我们可以同时试探所有路径, 当发现某条路径不通时直接放弃该路径不必回溯.

    (epsilon)闭包

    为了消除(epsilon)状态转移, 我们引入(epsilon)闭包的概念:

    从状态集T出发,不经任何字符可达到的状态的集合称为T的(epsilon)闭包, 记作(epsilon(T))

    建立一个集合V, 将T添加到V中, 遍历V中的每一个状态s, 将s可以通过(epsilon)状态转移到达的状态添加到V中, 最终得到的V即为T的(epsilon)闭包.

    {s2}的(epsilon)闭包为{s2, s4, s5}

    为了便于叙述, 我们将从状态集S出发通过条件a可以到达的下一状态全体记作smove(S, a).则$ smove(epsilon(T), epsilon) subset epsilon(T) $

    我们可以把(epsilon)闭包当做一个状态来看待:

    (a|b)*abb的NFA上识别输入序列abb:

    1. 计算初态集: $ epsilon({0}) = {0, 1, 2, 4, 7} = A $

    2. 由A出发经条件a到达: $ epsilon(smove(A,a)) = {1, 2, 3, 4, 6, 7, 8} = B $

    3. 由B出发经条件b到达: $ epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $

    4. 由C出发经条件b到达: $ epsilon(smove(C, b)) = {1, 2, 4, 5, 6, 7, 10} = D $

    5. 10为终态,接受

    识别abab:

    1. 计算初态集: $ epsilon({0}) = {0, 1, 2, 4, 7} = A $

    2. 由A出发经条件a到达: $ epsilon(smove(A,a)) = {1, 2, 3, 4, 6, 7, 8} = B $

    3. 由B出发经条件b到达: $ epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $

    4. 由C出发经条件a到达: $ epsilon(smove(C, a)) = {1, 2, 3, 4, 6, 7, 8} = B $

    5. 由B出发经条件b到达: $ epsilon(smove(B, b)) = {1, 2, 4, 5, 6, 7, 9} = C $

    未到达终态, 不接受

    子集构造法

    算法流程:

    1. 初始化数据结构: DFA自动机D, 状态集的集合DS, 状态转义关系集DT
      将epsilon({0})加入到DS中, 所有状态置为未标记
      当DS中仍有未标记的状态集T时执行循环:
      标记T
      遍历每一个字符a: // 只有可以从T中转移出去的字符才有意义
      令 S = epsilon(smove(T, a))
      若S非空:
      令DT(T, a) = S
      若S不在DS中:
      将S作为未标记的状态集加入DS

    示例, 由(a|b)*abb的NFA构造DFA:

    1. 初始化:$ A = epsilon({0}) = {0, 1, 2, 4, 7}; DS.append(A); $

    2. $ B = epsilon(smove(A, a)) = {1, 2, 3, 4, 6, 7, 8}; DS.append(B); DT(A, a) = B $

    3. $ C = epsilon(smove(A, b)) = {1, 2, 4, 5, 6, 7}; DS.append(C); DT(A, b) = C $

    4. $ S = epsilon(smove(B, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(B, a) = B $

    5. $ D = epsilon(smove(B, b)) = {1, 2, 3, 4, 6, 7, 8}; DS.append(D); DT(B, b) = D $

    6. $ S = epsilon(smove(C, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(C, a) = B $

    7. $ S = epsilon(smove(C, b)) = {1, 2, 4, 5, 6, 7}; S == C; DT(C, b) = C $

    8. $ S = epsilon(smove(D, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(D, a) = B $

    9. $ E = epsilon(smove(D, b)) = {1, 2, 4, 5, 6, 7, 10}; DS.append(E); DT(D, b) = E $

    10. $ S = epsilon(smove(E, a)) = {1, 2, 3, 4, 6, 7, 8}; S == B; DT(E, a) = B $

    11. $ S = epsilon(smove(E, b)) = {1, 2, 4, 5, 6, 7}; S == C; DT(E, b) = C $

    根据DS和DT绘制状态转移图:

    最小化DFA

    首先引入可区分的概念:对于DFA中任意两个状态s和t, 接受输入字符串w, 若s和t转移到不同状态则称w对于s和t是可区分的.

    最小化DFA的目的是使DFA的状态数最少, 定义一个DFA自动机的状态集为S, 终态集为F, 算法流程:

    初始化U = {S-F, F}
    遍历U中每一个状态集T:
    	初始化N = U
    	遍历T中任意状态的组合(s,t, ..):
    		若对于任意字符a, move(s,a)与move(t,a)均属于U中同一个状态集G:
    			将s,t划分入同一组, 使用新划分的组代替N中的G
    	若N == U退出, 以N作为最终划分
    	令U=N
    遍历U中没一个状态集T:
    	从T中选择一个状态s, 令T中出发的状态转移改为从s出发, 到T的状态转移改为转移到s
    清除所有死状态(只能转移到自身且不是终态)和不可达状态.
    

    我们用该算法简化上面的DFA:

    1. U = {ABCD, E}

    2. U = {ABC, D, E}

    3. U = {AC, B, D, E}

    AC可合并为一个状态:

  • 相关阅读:
    Spring(03)Spring IOC 概述
    Spring IoC Bean 创建方法总结
    Spring Boot 目录
    Spring 循环引用(三)AbstractFactoryBean 如何解决循环依赖
    Spring(02)重新认识 IoC
    极客时间
    Spring(01)特性总览
    Spring 核心编程思想目录
    Spring IOC 前世今生之 JDNI
    sharding-jdbc-core 源码分析
  • 原文地址:https://www.cnblogs.com/Finley/p/6011746.html
Copyright © 2020-2023  润新知