1. 程式人生 > 實用技巧 >CTRE-04-構建有限狀態機

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