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

posted @ 2020-12-19 18:59  RelaxDude  阅读(166)  评论(0)    收藏  举报