CTRE-03-Parser (2)

修补parse table

上次写完的parse_table只能接受正确的输入,但是不能拒绝一些错误输入。

我们再看一下parse table:

( ) * + ? | ch \(\epsilon\)
E (alt0) mod seq alt ch mod seq alt \(\epsilon\)
alt0 (alt0) mod seq alt ch mod seq alt
alt \(\epsilon\) | seq0 alt \(\epsilon\)
mod \(\epsilon\) \(\epsilon\) * + ? \(\epsilon\) \(\epsilon\) \(\epsilon\)
seq0 (alt0) mod seq ch mod seq
seq (alt0) mod seq \(\epsilon\) \(\epsilon\) ch mod seq \(\epsilon\)

问题在于,比如,E对应的行需要拒绝)*+?|这些输入字符,但是如下模版:

template <char C>
static auto f(E, character<C>) -> stack<character<C>, _char, mod, seq, alt>;

却能匹配任意字符。

其实C++20的concept可以实现模版参数的约束,只需要简单的用enable_if加个typename约束就可以了:compiler explorer演示

#include <type_traits>

template <char C>
concept not_terminal = requires() {
  typename std::enable_if<C != '(', bool>::type;
  typename std::enable_if<C != ')', bool>::type;
  typename std::enable_if<C != '*', bool>::type;
  typename std::enable_if<C != '+', bool>::type;
  typename std::enable_if<C != '?', bool>::type;
  typename std::enable_if<C != '|', bool>::type;
};

然后在parse_table中加上约束:

template <char C> requires not_terminal<C>
static auto f(E, character<C>) -> stack<character<C>, _char, mod, seq, alt>;

不过最简单的方法还是枚举,所以我们只需要这样暴力地列出reject就可以了:

// 我将上一篇中的struct ch改为了character,ch这个名称给了AST类型
static auto f(E, character<')'>) -> reject;
static auto f(E, character<'*'>) -> reject;
static auto f(E, character<'+'>) -> reject;
static auto f(E, character<'?'>) -> reject;
static auto f(E, character<'|'>) -> reject;
...

这样就解决了上一次留下的问题。

让Parser输出AST

AST的表示

AST全称是Abstract Syntax Tree,顾名思义,AST是一个树结构。编译期构建一个树结构是比较困难的,因为c++20以前,constexpr函数中不能使用new分配空间,那每一次新增节点的操作就需要进行一次对象的复制,不仅编译器会爆内存,代码编写也是噩梦。

所以我们使用表达式模版来代表AST,也就是一个模板套模板的类型,比如(abc)*|(efg)+的AST类型是:

alt<star<concat<ch<'a'>, ch<'b'>, ch<'c'>>>, plus<concat<ch<'e'>, ch<'f'>, ch<'g'>>>>

定义AST中的类型,注意?+都是其他类型的别名,不是一个独立的类型:

// single char
template <char C>
struct ch {};

// |
template <typename... Ts>
struct alter {};

// cdot
template <typename... Ts>
struct concat {};

// *
template <typename T>
struct star {};

// +
template <typename T>
using plus = concat<T, star<T>>;

// ?
template <typename T>
using opt = alter<epsilon, T>;

action类型

生成AST时需要另一个栈,所以parser中同时有两个栈,一个是parse stack,一个是生成AST用的栈。

生成AST的方法是在parse table中添加一些action符号,解析过程如果遇到action符号,我们就执行相应操作,比如将一个字符入栈;如果遇到non-terminal符号,就继续解析。

在parse table中插入action符号如下,在parse_table中添加相应的返回类型即可:

( ) * + ? | ch \(\epsilon\)
E (alt0) mod seq alt ch _char mod seq alt \(\epsilon\)
alt0 (alt0) mod seq alt ch _char mod seq alt
alt \(\epsilon\) | seq0 _alt alt \(\epsilon\)
mod \(\epsilon\) \(\epsilon\) * _star + _plus ? _opt \(\epsilon\) \(\epsilon\) \(\epsilon\)
seq0 (alt0) mod seq ch _char mod seq
seq (alt0) mod _concat seq \(\epsilon\) \(\epsilon\) ch _char mod seq \(\epsilon\)

那parser如何区分non-terminal符号与action符号?答案是空基类:

// AST action base type
struct AST_action {};

struct _char : AST_action {};  // push one char onto AST stack
struct _concat : AST_action {};
struct _alter : AST_action {};
struct _star : AST_action {};
struct _plus : AST_action {};
struct _opt : AST_action {};

接着用std::is_base_of来区分:

template <typename T>
static constexpr bool is_AST_action(T) {
    return std::is_base_of<AST_action, T>::value;
}

修改parser

能够区分符号的类别后,parser就可以根据parse栈顶的类型来决定继续解析或构建AST了:

template <int IDX = 0, typename StackT, typename ASTT>
static constexpr auto parse(StackT st, ASTT ast) {
    auto symbol = top(st);

    if constexpr (is_AST_action(symbol)) {
        // 构建AST
        // ...
        // ...
    } else {
        auto op = decltype(Grammar::f(symbol, fstr_at<IDX>())){};

        return next_op<IDX>(op, pop(st), ast);
    }
}

template <int IDX, typename StackT, typename ASTT>
static constexpr auto next_op(pass, StackT st, ASTT ast) {
    return parse<IDX>(st, ast);
}

// ...

注意这里AST要通过实参的形式在递归中传递,因为每次更新AST都是生成一个新的类型。

AST builder

现在我们来真正构建AST,分别来看6种action。我们同样利用重载规则来定义行为,builder接受一个旧栈,返回一个新栈。注意这里的栈不同于parser中的栈。

_char

_char表示将字符入栈,由于parse table中含_char的表项都是这样的:ch _char ...,要推入栈的字符在parser遇到_char时已经被ch“消耗”掉了。所以我们传入的character<C>是当前输入位置的上一位,这一点在parser中要注意。

另外,character<C>参数只在这里用到了,但是为了能够统一接口,其他重载也保留这个参数,但不会使用。

template <char C, typename... Ts>
static auto build_AST(_char, character<C>, stack<Ts...>) -> stack<ch<C>, Ts...>;

_concat

_concat表示多个类型(代表了表达式)的连接,因为模版参数数量可变,所以我们需要处理两种情况:连接两个类型和将一个类型添加到已有的连接中。

需要注意的是stack<...>左边是栈顶,栈顶的元素应该出现在连接的后端(右端)。

template <char C, typename T1, typename T2, typename... Ts>
static auto build_AST(_concat, character<C>, stack<T1, T2, Ts...>) -> stack<concat<T2, T1>, Ts...>;

template <char C, typename T, typename... Ts1, typename... Ts2>
static auto build_AST(_concat, character<C>, stack<T, concat<Ts1...>, Ts2...>) -> stack<concat<Ts1..., T>, Ts2...>;

_alter

_alter表示多个类型之间的选择,构建与_concat完全是一样的。

template <char C, typename T1, typename T2, typename... Ts>
static auto build_AST(_alter, character<C>, stack<T1, T2, Ts...>) -> stack<alter<T2, T1>, Ts...>;

template <char C, typename T, typename... Ts1, typename... Ts2>
static auto build_AST(_alter, character<C>, stack<T, alter<Ts1...>, Ts2...>) -> stack<alter<Ts1..., T>, Ts2...>;

_star_plus_opt

对单个元素处理就非常简单了,只需要给类型包上相应AST类型即可。

template <char C, typename T, typename... Ts>
static auto build_AST(_star, character<C>, stack<T, Ts...>) -> stack<star<T>, Ts...>;

template <char C, typename T, typename... Ts>
static auto build_AST(_plus, character<C>, stack<T, Ts...>) -> stack<plus<T>, Ts...>;

template <char C, typename T, typename... Ts>
static auto build_AST(_opt, character<C>, stack<T, Ts...>) -> stack<opt<T>, Ts...>;

修改parser

首先把构建AST的代码补上:

if constexpr (is_AST_action(symbol)) {
    auto new_ast = decltype(Grammar::build_AST(symbol, fstr_at<IDX - 1>(), ast)){};

    return parse<IDX>(pop(st), new_ast);
}

现在返回结果需要有两部分,表达式正确与否和AST类型,所以用一个pair包装一下:

template <int IDX, typename StackT, typename ASTT>
static constexpr auto next_op(accept, StackT, ASTT ast) {
    return std::make_pair(true, ast);
}

template <int IDX, typename StackT, typename ASTT>
static constexpr auto next_op(reject, StackT, ASTT ast) {
    return std::make_pair(false, ast);
}

最后把结果暴露给外界:

static constexpr auto result = parser::parse(stack<S>{}, stack<>{});

static constexpr auto correct = result.first;
using AST = decltype(top(result.second));

至此parser就完成了,我们可以获得ast对象了:

static constexpr fixed_string fstr("a*b+c?");
static_assert(parser<fstr, parse_table>::correct);
static constexpr ast = typename parser<fstr, parse_table>::AST{};

得到的AST是如下类型:

concat<star<ch<'a'> >, concat<ch<'b'>, star<ch<'b'> > >, alter<epsilon, ch<'c'> > >
posted @ 2020-11-17 16:17  RelaxDude  阅读(235)  评论(0)    收藏  举报