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了。