C++ 构建可组合有向图(Thompson';的扫描生成器构造算法)

C++ 构建可组合有向图(Thompson';的扫描生成器构造算法),c++,graph,compiler-construction,lexer,directed-graph,C++,Graph,Compiler Construction,Lexer,Directed Graph,我目前正在编写一个基于的扫描生成器,用于将正则表达式转换为NFA。基本上,我需要解析一个表达式并从中创建一个有向图。我通常将有向图存储为邻接列表,但这一次,我需要能够非常有效地将现有的有向图组合成新的有向图。我不能每次读一个新字符时都复制我的邻接列表 我正在考虑创建一个非常轻量级的NFA结构,它不会拥有自己的节点/状态 struct Transition { State* next_state; char transition_symbol; }; struct State { s

我目前正在编写一个基于的扫描生成器,用于将正则表达式转换为NFA。基本上,我需要解析一个表达式并从中创建一个有向图。我通常将有向图存储为邻接列表,但这一次,我需要能够非常有效地将现有的有向图组合成新的有向图。我不能每次读一个新字符时都复制我的邻接列表

我正在考虑创建一个非常轻量级的NFA结构,它不会拥有自己的节点/状态

struct Transition {
  State* next_state;
  char transition_symbol;
};

struct State {
  std::vector<Transition> transitions;
};

struct NFA {
  State* start_state;
  State* accepting_state;
};
在这里,我将使用移动语义来明确nfa0和nfa1不再用作独立的NFA(因为我修改了它们的内部状态)


这种方法有意义吗?还是有一个我还没有预料到的问题?如果真的有道理,那么这些州的所有者应该是什么?我还预计我的过渡会出现填充问题。在矢量中打包时,转换的大小将为16字节,而不是9字节(在64位体系结构上)。这是我应该担心的事情,还是这只是事情大计划中的噪音?(这是我的第一个编译器。我如下)

汤普森结构的本质是它创建了一个具有以下特征的NFA:

  • 最多有
    2个| R |
    状态,其中
    |R |
    是正则表达式的长度

  • 每个状态要么正好有一个用字符标记的输出转换,要么最多有两个ε转换。(也就是说,没有状态同时具有标记跃迁和ε跃迁。)

  • 后一个事实表明,将一个国家代表为

    struct State {
      std::vector<std::tuple<char, State*>> transitions;
    }
    
    (该公式不存储
    next
    中的跃迁数量,前提是我们可以使用哨兵值来表示
    next[1]
    不适用。或者,在这种情况下,我们可以只设置
    next[1]=next[0];
    。记住,它只对ε状态重要。)

    此外,由于我们知道NFA中只有2个| R |
    状态
    对象,因此我们可以用小整数替换
    状态*
    指针。这将对可以处理的正则表达式的大小设置某种限制,但是遇到千兆字节正则表达式是非常罕见的。使用连续整数而不是指针也将使某些图算法更易于管理,特别是对子集构造至关重要的传递闭包算法

    关于由汤普森算法构造的NFA的另一个有趣的事实是,状态的in度也被限制为2(同样,如果有两个in跃迁,则两者都将是ε跃迁)。这使我们能够避免过早地创建子机的最终状态(如果子机是连接的左侧参数,则不需要最终状态)。相反,我们可以用三个索引来表示子机器:开始状态的索引,以及最多两个内部状态的索引,一旦添加,这些状态将转换为最终状态

    我认为以上内容与Thompson最初的实现相当接近,尽管我确信他使用了更多的优化技巧。但值得一读的是Aho,Lam,Sethi&Ullman(“龙之书”)的第3.9节,该节描述了优化状态机结构的方法


    与理论简化无关,值得注意的是,除了关键字模式的trie之外,词汇分析中的大多数状态转换涉及字符集而不是单个字符,而且这些字符集通常相当大,尤其是如果词法分析的单位是Unicode码点而不是ascii字符。使用字符集而不是字符确实会使子集构造算法复杂化,但通常会显著减少状态计数。

    汤普森构造的本质是它创建了具有以下特征的NFA:

  • 最多有
    2个| R |
    状态,其中
    |R |
    是正则表达式的长度

  • 每个状态要么正好有一个用字符标记的输出转换,要么最多有两个ε转换。(也就是说,没有状态同时具有标记跃迁和ε跃迁。)

  • 后一个事实表明,将一个国家代表为

    struct State {
      std::vector<std::tuple<char, State*>> transitions;
    }
    
    (该公式不存储
    next
    中的跃迁数量,前提是我们可以使用哨兵值来表示
    next[1]
    不适用。或者,在这种情况下,我们可以只设置
    next[1]=next[0];
    。记住,它只对ε状态重要。)

    此外,由于我们知道NFA中只有2个| R |
    状态
    对象,因此我们可以用小整数替换
    状态*
    指针。这将对可以处理的正则表达式的大小设置某种限制,但是遇到千兆字节正则表达式是非常罕见的。使用连续整数而不是指针也将使某些图算法更易于管理,特别是对子集构造至关重要的传递闭包算法

    关于由汤普森算法构造的NFA的另一个有趣的事实是,状态的in度也被限制为2(同样,如果有两个in跃迁,则两者都将是ε跃迁)。这使我们能够避免过早地创建子机的最终状态(如果子机是连接的左侧参数,则不需要最终状态)。相反,我们可以用三个索引来表示子机器:开始状态的索引,以及最多两个内部状态的索引,一旦添加,这些状态将转换为最终状态

    我认为以上内容与Thompson最初的实现相当接近,尽管我确信他使用了更多的优化技巧
    enum class StateType { EPSILON, IMPORTANT };
    struct State {
      StateType type;
      char      label;
      State*    next[2];
    };