构建可组合有向图(扫描仪生成器的汤普森构造算法)

Building composable directed graphs (Thompson's construction algorithm for scanner generator)

本文关键字:汤普森 算法 有向图 可组合 扫描仪 构建      更新时间:2023-10-16

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

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

struct Transition {
State* next_state;
char transition_symbol;
};
struct State {
std::vector<Transition> transitions;
};
struct NFA {
State* start_state;
State* accepting_state;
};

这将允许我简单地重新分配指针来创建新的NFA。我的所有状态都将存储在一个中心位置(NFABuilder?(。合成将通过以下外部功能完成:

NFA create_trivial_nfa(char symbol) {
State* start_state = new State();
State* accepting_state = new State();
start_state->transitions.emplace_back(accepting_state, symbol);
// Something must own start_state and accepting_state
return NFA{start_state, accepting_state};
}
NFA concatenate_nfas(NFA&& nfa0, NFA&& nfa1) {
nfa0.accepting_state->transitions.emplace_back(nfa1.start_state, '');
return NFA{nfa0.start_state, nfa1.accepting_state};
}

在这里,我将使用move语义来明确nfa0和nfa1不再用作独立的NFA(因为我修改了它们的内部状态(。

这种方法有意义吗?还是存在我尚未预料到的问题?如果这真的有意义,那么所有这些州的所有者应该是什么?我还预计我的过渡会出现填充问题。当封装在矢量中时,Transition的大小将为16字节,而不是9字节(在64位架构中(。这是我应该担心的事情吗?还是这只是大计划中的噪音?(这是我的第一个编译器。我正在学习Cooper&Torczon的Engineering a compiler(

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

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

  2. 每个状态要么只有一个用字符标记的输出转换,要么最多有两个&ε;过渡。(也就是说,没有一个状态同时具有标记跃迁和ε跃迁。(

后一个事实表明,将一个状态表示为

struct State {
std::vector<std::tuple<char, State*>> transitions;
}

(这是代码的一个缩写(是一个非常高的开销表示,其中开销与用于保持一个或两个转换的std::vector的开销有关,而不是与单个转换的填充有关。此外,上述表示没有提供用于表示ε;转换,除非目的是为ε(从而使得无法在正则表达式中使用该字符代码(。

可能是一种更实用的表示方式

enum class StateType { EPSILON, IMPORTANT };
struct State {
StateType type;
char      label;
State*    next[2];
};

(该公式不存储next中的转换次数,假设我们可以使用哨兵值来指示next[1]不适用。或者,在这种情况下,我们可以只设置next[1] = next[0];。记住,这只对ε状态重要。(

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

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

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

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