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'> > >