3

亲爱的灵气专家。

我玩过 Spirit Qi 中的 MiniC 示例,并注意到表达式语法中“空”AST 节点的问题。它将再次生成只包含一个“表达式”类型的操作数的“表达式”节点。

我认为问题是由于运算符优先级的递归规则定义:

// expression.hpp
    qi::rule<Iterator, ast::expression(), skipper<Iterator> >
        expr, equality_expr, relational_expr,
        logical_or_expr, logical_and_expr,
        additive_expr, multiplicative_expr
        ;

// expression_def.hpp
    expr =
        logical_or_expr.alias()
        ;

    logical_or_expr =
            logical_and_expr
        >> *(logical_or_op > logical_and_expr)
        ;

    logical_and_expr =
            equality_expr
        >> *(logical_and_op > equality_expr)
        ;

// ast.hpp

typedef boost::variant<
        nil
      , bool
      , unsigned int
      , identifier
      , boost::recursive_wrapper<unary>
      , boost::recursive_wrapper<function_call>
      , boost::recursive_wrapper<expression>
    >
operand;
/*...*/
struct expression
{
    operand first;
    std::list<operation> rest;
};

当logical_or_expr 递归到logical_and_expr 时,logical_and_expr 将返回一个表达式()。由于属性传播是 Spirit,logical_or_expr 然后将此表达式分配给它的表达式的“第一个”成员。

我认为这就是生成所有这些“空”表达式节点的原因。然而,我觉得这很讨厌,并想摆脱它们,但尚未成功。以前有没有人找到一个不错的解决方案?

我认为如果表达式由二元操作和一元操作组成,那将是可能的。这也将具有将操作和操作数保持在相同结构(伪代码)中的优点:

struct binary_op
{
    optype type;
    operand op1;
    operand op2;
}

struct unary_op
{
    optype type;
    operand op1;
}

struct eval_op
{
    operand op1;
}

typedef boost::variant<binary_op,
                       unary_op,
                       eval_op>
expression;

typedef boost::variant<int,
                       bool,
                       boost::recursive_wrapper<expression> >
operand;

但是,我相信在纸上播放后我仍然会遇到这个“空节点”问题。我好像在追自己的尾巴。

有谁知道如何解决这个问题?

编辑

a > b

将解析为:

    expression (from logical_or_op) // Empty expression (forward only)
(rest)  -/  \- (first) expression (from logical_and_op) // Empty expression (forward only)
            (rest)  -/  \- (first) expression (from equality_op) // Empty expression (forward only)
                    (rest)  -/ \- (first) expression (from relational_op) // "Correct" expression
                            (first) a   -/      \- (rest)
                                                    [0] operator_: op_greater
                                                        operand_: b

所需的输出将是:

            expression (from relational_op)
(first) a   -/      \- (rest)
                        [0] operator_: op_greater
                            operand_: b 

编辑2

我上传了一个修改过的 mini-c 版本,它打印了一个表达式的生成表达式树:

关联

如果您查看包含的 A.avxh 文件,它包含要解析的表达式。在 main.cpp 的第 67 行设置断点,并观察它在此处递归执行的频率。

4

1 回答 1

2

这是因为所有规则都暴露了一个“原始” ast::expression,这是一个固定类型。

在这个示例中,它显然是一个简单的选择:好处是

  • 通过使所有表达式节点的类型相同,树的访问代码可以简单而统一。
  • 所有规则都具有相同的“签名”并遵循相同的模式。这很容易推理。
  • 在这个特定的示例 (mini_c) 中,代码生成阶段通过继承相同的简单性而受益。

拥有更灵活的 AST 以更紧密地遵循语义的通常方法是制作expression一个变体:这样每个表达式都可以直接包含实际的“具体”子表达式类型,而不是“涉水”通过节点类型的中间级别,只是为了模仿语法结构而不是语义

我想我在这个网站上有几个这样的例子和相应的语法,以后看看能不能链接一个。

事实上,我认为typedef variant<...> statement同一个例子中的 ( ast.hpp) 并不是这种方法的一个坏例子。

相关链接:

目前,如果您不想更改语法(以免“失去”简单性),您可以改为对 AST 进行转换(可以说是对表达式进行“简化”传递)。

我会在接下来的一个小时内看看我能想出什么。

我刚刚重构了语法,这样它就不会导致如此深的嵌套。

  1. 首先,让我们将测试简化为一个独立的测试平台,它只解析表达式并展示一个简单的表达式 ( "42") 如何解析为深度嵌套的 AST:http ://coliru.stacked-crooked.com/a/5467ca41b0ac1d03

    <expr>
      ...
      <success></success>
      <attributes>[[[[[[[42, []], []], []], []], []], []]]</attributes>
    </expr>
    
  2. 接下来,让我们消除根本问题:语法返回一个ast::expression在许多情况下过于繁重的不变类型 ( )。相反,我们想返回一个ast::operand(它是变体,可以包含相同的ast::expression节点):http ://coliru.stacked-crooked.com/a/00e43b1f61db018c

  3. 最后,我们希望所有规则也成为变体,并在伪代码中返回一个expressioniff 存在尾随操作,或者在另一种情况下返回一个单独的:operand

    logical_or_expr = 
          (logical_and_expr >> +(logical_or_op > logical_and_expr)
        | logical_and_expr
        ;
    

    注意微妙的使用 if+(...)而不是*(...)强制至少一个结尾的logical_or操作

    现在,必须告诉 Spirit 如何将第一个分支分配给一个operand属性。qi::attr_cast<ast::expression>(...)应该是这里的修复,但遗憾的是我遇到了未定义的行为(这花了最多时间)。我选择了一个更详细的解决方案:

    _logical_or_expr = logical_and_expr    >> +(logical_or_op     > logical_and_expr) ;
    logical_or_expr  = _logical_or_expr | logical_and_expr;
    

    正如您可能看到的那样,它是一样的,但是将第一个分支提取到单独的规则中,因此我们可以声明规则以公开所需的属性:

    qi::rule<It, ast::operand(),    Skipper> logical_or_expr;
    qi::rule<It, ast::expression(), Skipper> _logical_or_expr;
    

    实际上,对子表达式的每个优先级执行此操作会导致以下结果:

    _logical_or_expr     = logical_and_expr    >> +(logical_or_op     > logical_and_expr) ;
    _multiplicative_expr = unary_expr          >> *(multiplicative_op > unary_expr) ;
    _additive_expr       = multiplicative_expr >> +(additive_op       > multiplicative_expr) ;
    _relational_expr     = additive_expr       >> +(relational_op     > additive_expr) ;
    _equality_expr       = relational_expr     >> +(equality_op       > relational_expr) ;
    _logical_and_expr    = equality_expr       >> +(logical_and_op    > equality_expr) ;
    
    logical_or_expr     = _logical_or_expr     | logical_and_expr        ;
    logical_and_expr    = _logical_and_expr    | equality_expr           ;
    equality_expr       = _equality_expr       | relational_expr         ;
    relational_expr     = _relational_expr     | additive_expr           ;
    additive_expr       = _additive_expr       | multiplicative_expr     ;
    multiplicative_expr = _multiplicative_expr | attr_cast<ast::operand, ast::operand> (unary_expr) ;
    

最终结果在这里:http : //coliru.stacked-crooked.com/a/8539757bb02fca34(遗憾的是它对 Coliru 来说太多了),它打印:

<expr>
  ...
  </logical_or_expr>
  <success></success>
  <attributes>[[42, []]]</attributes>
</expr>

CAVEAT请注意,这种调整不会使解析器更高效!事实上,它只会导致大量的回溯(调试输出计数 925 行,而不是步骤 1中的 45 行(!!))。

现在,使用前瞻断言和/或语义操作将有一些优化空间,但通常您将不得不考虑优化 AST 存储将花费 CPU 时间。

于 2013-10-31T19:29:21.573 回答