CTRE-04-构建有限状态机
最前面说到,现在的正则表达式引擎一般是用有限状态机(NFA)进行匹配。前面我们已经用parser得到了AST,现在我们来构建NFA。
一个有限状态机由5元组描述:
- 有限个状态的集合
- 一个起始状态
- 终止状态的集合
- 输入字符的集合
- 状态转移函数的集合
输入字符集合不需要我们定义。状态我们可以用一个整型数表示,并且规定起始状态总是状态0,状态转移我们可以用一个结构体表示。那么现在的问题就是,如何在编译期表示集合这样的数据结构。
集合
其实我们没有必要实现一个集合数据结构,因为不需要集合的去重特性,编译期数组就足够了。
我们只需要实现一个支持constexpr的array:
template <typename T, int N>
struct array {
T _data[N] = {};
constexpr int size() const {
return N;
}
constexpr T operator[](int idx) const {
return _data[idx];
}
constexpr T& operator[](int idx) {
return _data[idx];
}
constexpr const T* begin() const {
return _data;
}
constexpr const T* end() const {
return &(_data[N]);
}
};
array相比其他容器特殊的一点是,array没有constructor,所以花括号初始化式的写法不同,后面应用时可以看到。
NFA进行状态转移时要搜索集合,所以我们另外加入一个排序的方法便于二分搜索。这里偷懒,只写插入排序。
// returns a new constexpr array in ascending order
template <typename T, int N>
template <typename CMP>
constexpr auto array<T, N>::sorted(CMP cmp) const {
array<T, N> res = *this;
if constexpr (N < 2)
return res;
else {
for (int i = 1; i < N; i++) {
for (int j = i; j > 0; j--) {
if (!cmp(res._data[j - 1], res._data[j])) {
T tmp = res._data[j - 1];
res._data[j - 1] = res._data[j];
res._data[j] = tmp;
} else {
break;
}
}
}
return res;
}
}
注意返回值是一个新的数组,因为编译期计算不能修改数据,只能复制数据,所以要改变数据只能创建一个新的对象。
NFA
先定义class NFA
,这里只定义了状态转移函数集合和终止状态集合。
idx_t
和idx_fs
用于跟踪两个array的数据插入位置。
state_count()
用于计算总状态数,因为状态使用由0开始的连续的整数表示,所以扫描一遍状态转移集合就能知道状态数。
template <int N_T, int N_FS>
class finite_automata {
private:
int idx_t = 0, idx_fs = 0;
public:
array<transition, N_T> transitions;
array<int, N_FS> final_states;
constexpr finite_automata() {}
constexpr finite_automata(array<transition, N_T> t, array<int, N_FS> fs)
: transitions(t), final_states(fs) {
this->sort();
}
constexpr finite_automata(const finite_automata<N_T, N_FS>& other)
: transitions(other.transitions), final_states(other.final_states), idx_t(other.idx_t), idx_fs(other.idx_fs) {}
constexpr int size_transition() const {
return N_T;
}
constexpr int size_final_state() const {
return N_FS;
}
constexpr void add_transition(const transition& t) {
transitions[idx_t] = t;
idx_t++;
}
constexpr void add_final_state(int fs) {
final_states[idx_fs] = fs;
idx_fs++;
}
constexpr int state_count() const;
// 排序两个array
constexpr void sort();
// 二分查找src的状态转移,返回在状态转移array中最左侧项的索引
constexpr int lower_idx_in_trans(int src) const;
// 检查fs是否在终止状态集合中
constexpr bool is_final_state(int fs) const;
};
其中状态转移的定义:
这里有一个是否是epsilon转移的标记,方便判断。
struct transition {
int src;
int dst;
char char_to_match;
bool is_epsilon;
constexpr transition(int src = -1, int dst = -1, char c = '\0')
: src(src), dst(dst), char_to_match(c), is_epsilon(c == '\0') {}
constexpr bool match(char c) const {
return c == char_to_match;
}
};
构建NFA
基础的两个NFA
从AST构建NFA的过程就是将很多小NFA连接起来,最简单的NFA只有两种,空或只有一个状态转移。
// 有括号的状态表示终止状态
// (0)
static constexpr finite_automata<0, 1> FA_epsilon{ {}, { 0 } };
// 0 --'a'--> (1)
template <char C>
static constexpr finite_automata<1, 1> FA_char{ { { { 0, 1, C } } }, { 1 } };
顺便解释一下初始化式的括号:
FA_char{ { { { 0, 1, C } } }, { 1 } }
|<- ->| -- 传给transition的初始化参数
|<- ->| -- 传给array的数据成员
|<- 传给array ->| |<->| -- 传给array,由于数据成员是int,标准规定可以省略数据成员的一层括号
|<-finite_automata的初始化参数->|
Builders
解析AST构建NFA的实现类似于parser,也是递归和重载的形式。
至于为什么要写成FA_concat<build_FA(Ts{})...>::res
这样的形式,见下文connector的实现。
// 将concat中的FA用concat连接器串联起来,并递归构建
template <typename... Ts>
constexpr auto& build_FA(concat<Ts...>) {
return FA_concat<build_FA(Ts{})...>::res;
}
// 将alter中的FA用alter连接器连接,并递归构建
template <typename... Ts>
constexpr auto& build_FA(alter<Ts...>) {
return FA_alter<build_FA(Ts{})...>::res;
}
template <typename T>
constexpr auto& build_FA(star<T>) {
return FA_star<build_FA(T{})>::res;
}
// 递归出口,遇到字符
template <char C>
constexpr auto& build_FA(ch<C>) {
return FA_char<C>;
}
// 递归出口,遇到空标记
constexpr auto& build_FA(epsilon) {
return FA_epsilon;
}
Connectors
现在再来实现真正连接NFA的部分。
注意到我没有直接写一个函数,而是将函数包装进一个结构体里,然后将函数的输出保存在一个static成员变量中。这是为了强制编译器在编译期计算出函数的结果,否则可能出现编译器将计算移到运行期的情况;计算结果同时相当于一个缓存,遇到相同的调用可以避免重复计算,加快编译期计算的速度;最后还为了便于实现可变模板参数接口。
// 用一个epsilon连接LHS终态到RHS初态
// from
// 0 --...--> (n1) 0 --...--> (n2)
// to
// 0 --...--> n1 --epsilon--> n1+1 --...--> (n2+n1+1)
template <auto& LHS, auto& RHS, auto&... FAs>
struct FA_concat {
template <int N_T1, int N_FS1, int N_T2, int N_FS2>
static constexpr auto f(const finite_automata<N_T1, N_FS1>& lhs, const finite_automata<N_T2, N_FS2>& rhs) {
finite_automata<N_T1 + N_T2 + N_FS1, N_FS2> res;
int l_st_cnt = lhs.state_count();
// copy lhs's transitions
for (transition t : lhs.transitions) {
res.add_transition(t);
}
// copy rhs's transitions
for (transition t : rhs.transitions) {
t.src += l_st_cnt;
t.dst += l_st_cnt;
res.add_transition(t);
}
// connect lhs's final states to rhs
for (int fs : lhs.final_states) {
res.add_transition({ fs, l_st_cnt });
}
// copy final states
for (int fs : rhs.final_states) {
res.add_final_state(fs + l_st_cnt);
}
res.sort();
return res;
}
template <typename T1, typename T2, typename... Ts>
static constexpr auto f(T1 t1, T2 t2, Ts... ts) {
return f(f(t1, t2), ts...);
}
static constexpr auto res = f(LHS, RHS, FAs...);
};
// 合并初态
// from
// 0 --...--> (n1) 0 --...--> (n2)
// to
// 0 --...--> (n1)
// |--...---> (n2+n1)
template <auto& LHS, auto& RHS, auto&... FAs>
struct FA_alter {
...
};
// 终态增加一个epsilon连接到初态
// from
// 0 --...--> (n1)
// to
// 0 --...--> (n1)
// ^--epsilon--|
template <auto& FA>
struct FA_star {
template <int N_T, int N_FS>
static constexpr auto f(const finite_automata<N_T, N_FS>& fa) {
finite_automata<N_T + N_FS, N_FS> res;
for (transition t : fa.transitions) {
res.add_transition(t);
}
for (int fs : fa.final_states) {
res.add_transition({ fs, 0 });
}
res.sort();
return res;
}
static constexpr auto res = f(FA);
};
至此,我们就可以使用build_FA(ast)
得到NFA了。