Boost spirit compile error for trivial grammar

2019-07-15 04:32发布

问题:

I am trying to compile a parser with the following rules:

else_statement =
    lit("else") > statement;

if_statement =
    lit("if") >> '(' >> expression >> ')' >> statement >> -else_statement;

The attribute of else_statement is statement, as is the statement rule that it consumes. The attribute of if_statement is a struct with members respectively types expression, statement and an optional statement (boost::optional<statement>).

Using the following BOOST_FUSION_ADAPT_STRUCT's:

BOOST_FUSION_ADAPT_STRUCT(ast::statement, m_statement_node)
BOOST_FUSION_ADAPT_STRUCT(ast::if_statement, m_condition, m_then, m_else)

where m_statement_node is a boost::variant for different statements that are possible.

I'd expect that if an else_statement is present, it will be put into the boost::optional<statement>, since the attribute of else_statement is statement. And this does work if I comment out the lit("else") > in the else_statement rule! But with the lit("else") present something strange happens: now boost::spirit is trying to fit a statement into the member of the optional statement (the boost::variant) which obviously won't compile because that only takes an A or B.

The resulting compile error looks like this:

/usr/include/boost/variant/variant.hpp:1534:38: error: no matching function for call to ‘boost::variant<ast::A, ast::B>::initializer::initialize(void*, const ast::statement&)’

What am I doing wrong? How can I solve this?

Below a complete test snippet that shows the error (and compiles when the lit("else") > is comment out).

// File: so.cpp
// Compile as: g++ -std=c++11 so.cpp

#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <boost/optional/optional_io.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace ast
{

struct A { int a; friend std::ostream& operator<<(std::ostream& os, A const&) { return os << "A"; } };
struct B { int b; friend std::ostream& operator<<(std::ostream& os, B const&) { return os << "B"; } };
struct expression { int e; friend std::ostream& operator<<(std::ostream& os, expression const&) { return os << "expression"; } };

using statement_node = boost::variant<A, B>;

struct statement
{
  statement_node m_statement_node;

  friend std::ostream& operator<<(std::ostream& os, statement const& statement)
      { return os << "STATEMENT:" << statement.m_statement_node; }
};

struct if_statement
{
  expression m_condition;
  statement m_then;
  boost::optional<statement> m_else;

  friend std::ostream& operator<<(std::ostream& os, if_statement const& if_statement)
  {
    os << "IF_STATEMENT:" << if_statement.m_condition << "; " << if_statement.m_then;
    if (if_statement.m_else)
      os << "; " << if_statement.m_else;
    return os;
  }
};

} // namespace ast

BOOST_FUSION_ADAPT_STRUCT(ast::expression, e)
BOOST_FUSION_ADAPT_STRUCT(ast::A, a)
BOOST_FUSION_ADAPT_STRUCT(ast::B, b)
BOOST_FUSION_ADAPT_STRUCT(ast::statement, m_statement_node)
BOOST_FUSION_ADAPT_STRUCT(ast::if_statement, m_condition, m_then, m_else)

namespace client
{

namespace qi = boost::spirit::qi;
namespace ascii = boost::spirit::ascii;

template <typename Iterator>
class test_grammar : public qi::grammar<Iterator, ast::if_statement(), qi::space_type>
{
 private:
  template<typename T> using rule = qi::rule<Iterator, T(), qi::space_type>;

  rule<ast::A>                                     a;
  rule<ast::B>                                     b;
  rule<ast::statement>                             statement;
  rule<ast::statement>                             else_statement;
  rule<ast::if_statement>                          if_statement;
  rule<int>                                        expression;

 public:
  test_grammar() : test_grammar::base_type(if_statement, "result_grammar")
  {
    using namespace qi;

    statement =
      a | b;

    else_statement =
      lit("else") > statement;

    if_statement =
      lit("if") >> '(' >> expression >> ')' >> statement >> -else_statement;

    expression =
        int_;

    a = 'A';
    b = 'B';

    BOOST_SPIRIT_DEBUG_NODES(
        (statement)
        (else_statement)
        (if_statement)
        (expression)
        (a)
        (b)
    );
  }
};

} // namespace client

int main()
{
  std::string const input{"if (1) A B"};
  using iterator_type = std::string::const_iterator;
  using test_grammar = client::test_grammar<iterator_type>;
  namespace qi = boost::spirit::qi;

  test_grammar program;
  iterator_type iter{input.begin()};
  iterator_type const end{input.end()};
  ast::if_statement out;
  bool r = qi::phrase_parse(iter, end, program, qi::space, out);

  if (!r || iter != end)
  {
    std::cerr << "Parsing failed." << std::endl;
    return 1;
  }
  std::cout << "Parsed: " << out << std::endl;
}

回答1:

Automatic attribute propagation rules have some trouble with Fusion sequences that consist of a single element. You can work around it here by declaring:

rule<ast::statement_node> statement;

(changing from ast::statement to ast::statement_node).

This works: Live On Coliru

Alternative Workaround

The more tedious workaround is to avoid having a single-element fusion sequence there. You can add a dummy field to statement:

struct statement
{
    statement_node m_statement_node;
    int dummy;

    friend std::ostream& operator<<(std::ostream& os, statement const& statement)
    { return os << "STATEMENT:" << statement.m_statement_node; }
};

BOOST_FUSION_ADAPT_STRUCT(ast::statement, m_statement_node, dummy)

And then add a value for it:

statement = (a | b) >> attr(42);

This removes confusion as well.

Live On Wandbox

// File: so.cpp
// Compile as: g++ -std=c++11 so.cpp
//#define BOOST_SPIRIT_DEBUG

#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/include/std_pair.hpp>
#include <boost/optional/optional_io.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace ast
{

    struct A { int a; friend std::ostream& operator<<(std::ostream& os, A const&) { return os << "A"; } };
    struct B { int b; friend std::ostream& operator<<(std::ostream& os, B const&) { return os << "B"; } };
    struct expression { int e; friend std::ostream& operator<<(std::ostream& os, expression const&) { return os << "expression"; } };

    using statement_node = boost::variant<A, B>;

    struct statement
    {
        statement_node m_statement_node;
        int dummy;

        friend std::ostream& operator<<(std::ostream& os, statement const& statement)
        { return os << "STATEMENT:" << statement.m_statement_node; }
    };

    struct if_statement
    {
        expression m_condition;
        statement m_then;
        boost::optional<statement> m_else;

        friend std::ostream& operator<<(std::ostream& os, if_statement const& if_statement)
        {
            os << "IF_STATEMENT:" << if_statement.m_condition << "; " << if_statement.m_then;
            if (if_statement.m_else)
                os << "; " << if_statement.m_else;
            return os;
        }
    };

} // namespace ast

    BOOST_FUSION_ADAPT_STRUCT(ast::expression, e)
    BOOST_FUSION_ADAPT_STRUCT(ast::A, a)
    BOOST_FUSION_ADAPT_STRUCT(ast::B, b)
    BOOST_FUSION_ADAPT_STRUCT(ast::statement, m_statement_node, dummy)
    BOOST_FUSION_ADAPT_STRUCT(ast::if_statement, m_condition, m_then, m_else)

    namespace client
{

    namespace qi = boost::spirit::qi;
    namespace ascii = boost::spirit::ascii;

    template <typename Iterator>
        class test_grammar : public qi::grammar<Iterator, ast::if_statement(), qi::space_type>
    {
        private:
            template<typename T> using rule = qi::rule<Iterator, T(), qi::space_type>;

            rule<ast::A>            a;
            rule<ast::B>            b;
            rule<ast::statement> statement;
            rule<ast::statement> else_statement;
            rule<ast::if_statement> if_statement;
            rule<int>               expression;

        public:
            test_grammar() : test_grammar::base_type(if_statement, "result_grammar")
        {
            using namespace qi;

            statement = (a | b) >> attr(42);

            else_statement = lit("else") > statement;

            if_statement = lit("if") >> '(' >> expression >> ')' >> statement >> -else_statement;

            expression = int_;

            a = 'A' >> attr(1);
            b = 'B' >> attr(2);

            BOOST_SPIRIT_DEBUG_NODES( (statement) (else_statement) (if_statement) (expression) (a) (b));
        }
    };

} // namespace client

int main()
{
    for (std::string const input : {
                "if (1) A else B",
            }) 
    {
        using iterator_type = std::string::const_iterator;
        using test_grammar  = client::test_grammar<iterator_type>;
        namespace qi        = boost::spirit::qi;

        test_grammar program;
        iterator_type iter = input.begin(), end = input.end();
        ast::if_statement out;
        bool r = qi::phrase_parse(iter, end, program, qi::space, out);

        if (!r || iter != end)
        {
            std::cerr << "Parsing failed." << std::endl;
            return 1;
        }
        std::cout << "Parsed: " << out << std::endl;
    }
}

Prints

Parsed: IF_STATEMENT:expression; STATEMENT:A;  STATEMENT:B

Background

Note, though, that you get the same confusion back if you mistakenly make the else_statement rule also the same length:

else_statement = lit("else") > statement > attr(42); // this is wrong

Of course that doesn't actually make sense, but the error message DOES help in explaining what the real problem under the hood is (if the "upstream" fusion sequence appears "compatible" then it is "deconstructed" for propagation). The relevant comment in qi/nonterminal/rule.hpp:

// do up-stream transformation, this integrates the results
// back into the original attribute value, if appropriate
traits::post_transform(attr_param, attr_);