Search code examples
c++boostboost-spiritboost-spirit-qi

Boost Spirit template specialization failure


Below is a very compact version of a grammar I'm trying to write using boost::spirit::qi.
Environment: VS2013, x86, Boost1.64
When #including the header file, the compiler complains about the line

rBlock = "{" >> +(rInvocation) >> "}";

with a very long log (I've only copied the beginning and the end):

more than one partial specialization matches the template argument list ...
...
see reference to function template instantiation 'boost::spirit::qi::rule &boost::spirit::qi::rule::operator =>(const Expr &)' being compiled

Where is my mistake?

The header file:

//mygrammar.h
#pragma once 
#include <boost/spirit/include/qi.hpp>

namespace myNS
{

    typedef std::string Identifier;
    typedef ::boost::spirit::qi::rule <const char*, Identifier()> myIdentifierRule;

    typedef ::boost::variant<char, int> Expression;
    typedef ::boost::spirit::qi::rule <const char*, Expression()> myExpressionRule;

    struct IdntifierEqArgument
    {
        Identifier ident;
        Expression arg;
    };

    typedef ::boost::variant < IdntifierEqArgument, Expression >  Argument;
    typedef ::boost::spirit::qi::rule <const char*, Argument()> myArgumentRule;

    typedef ::std::vector<Argument> ArgumentList;
    typedef ::boost::spirit::qi::rule <const char*, myNS::ArgumentList()> myArgumentListRule;


    struct Invocation
    {
        Identifier identifier;
        ::boost::optional<ArgumentList> args;
    };
    typedef ::boost::spirit::qi::rule <const char*, Invocation()> myInvocationRule;


    typedef ::std::vector<Invocation> Block;
    typedef ::boost::spirit::qi::rule <const char*, myNS::Block()> myBlockRule;

}
BOOST_FUSION_ADAPT_STRUCT(
    myNS::IdntifierEqArgument,
    (auto, ident)
    (auto, arg)
    );
BOOST_FUSION_ADAPT_STRUCT(
    myNS::Invocation,
    (auto, identifier)
    (auto, args)
    );

namespace myNS
{
    struct myRules
    {
        myIdentifierRule rIdentifier;
        myExpressionRule rExpression;
        myArgumentRule rArgument;
        myArgumentListRule rArgumentList;
        myInvocationRule rInvocation;
        myBlockRule rBlock;

        myRules()
        {
            using namespace ::boost::spirit;
            using namespace ::boost::spirit::qi;

            rIdentifier = as_string[((qi::alpha | '_') >> *(qi::alnum | '_'))]; 
            rExpression = char_ | int_;
            rArgument = (rIdentifier >> "=" >> rExpression) | rExpression;
            rArgumentList = rArgument >> *("," >> rArgument);
            rInvocation = rIdentifier >> "(" >> -rArgumentList >> ")";
            rBlock = "{" >> +(rInvocation) >> "}";
        }
    };
}

Solution

  • I'm not exactly sure where the issue is triggered, but it clearly is a symptom of too many ambiguities in the attribute forwarding rules.

    Conceptually this could be triggered by your attribute types having similar/compatible layouts. In language theory, you're looking at a mismatch between C++'s nominative type system versus the approximation of structural typing in the attribute propagation system. But enough theorism :)

    I don't think attr_cast<> will save you here as it probably uses the same mechanics and heuristics under the hood.

    It drew my attention that making the ArgumentList optional is ... not very useful (as an empty list already accurately reflects absense of arguments).

    So I tried simplifying the rules:

    rArgumentList = -(rArgument % ',');
    rInvocation   = rIdentifier >> '(' >> rArgumentList >> ')';
    

    And the declared attribute type can be simply ArgumentList instead of boost::optional::ArgumentList.

    This turns out to remove the ambiguity when propagating into the vector<Invocation>, so ... you're saved.

    If this feels "accidental" to you, you should! What would I do if this hadn't removed the ambiguity "by chance"? I'd have created a semantic action to propagate the Invocation by simpler mechanics. There's a good chance that fusion::push_back(_val, _1) or similar would have worked.

    See also Boost Spirit: "Semantic actions are evil"?

    Review And Demo

    In the cleaned up review here I present a few fixes/improvements and a test run that dumps the parsed AST.

    1. Separate AST from parser (you don't want use qi in the AST types. You specifically do not want using namespace directives in the face of generic template libraries)
    2. Do not use auto in the adapt macros. That's not a feature. Instead, since you can ostensibly use C++11, use the C++11 (decltype) based macros

      BOOST_FUSION_ADAPT_STRUCT(myAST::IdntifierEqArgument, ident,arg);
      BOOST_FUSION_ADAPT_STRUCT(myAST::Invocation, identifier,args);
      
    3. AST is leading (also, prefer c++11 for clarity):

      namespace myAST {
          using Identifier = std::string;
          using Expression = boost::variant<char, int>;
      
          struct IdntifierEqArgument {
              Identifier ident;
              Expression arg;
          };
      
          using Argument = boost::variant<IdntifierEqArgument, Expression>;
      
          using ArgumentList = std::vector<Argument>;
      
          struct Invocation {
              Identifier identifier;
              ArgumentList args;
          };
      
          using Block = std::vector<Invocation>;
      }
      

      It's nice to have the definitions separate

    4. Regarding the parser,

      • I'd prefer the qi::grammar convention. Also,
      • You didn't declare any of the rules with a skipper. I "guessed" from context that whitespace is insignificant outside of the rules for Expression and Identifier.
      • Expression ate every char_, so also would eat ')' or even '3'. I noticed this only when testing and after debugging with:

        //#define BOOST_SPIRIT_DEBUG
        BOOST_SPIRIT_DEBUG_NODES((start)(rBlock)(rInvocation)(rIdentifier)(rArgumentList)(rArgument)(rExpression))
        

        I highly recommend using these facilities

    5. All in all the parser comes down to

      namespace myNS {
          namespace qi = boost::spirit::qi;
      
          template <typename Iterator = char const*>
          struct myRules : qi::grammar<Iterator, myAST::Block()> {
      
              myRules() : myRules::base_type(start) {
                  rIdentifier   = qi::raw [(qi::alpha | '_') >> *(qi::alnum | '_')];
                  rExpression   = qi::alpha | qi::int_;
                  rArgument     = (rIdentifier >> '=' >> rExpression) | rExpression;
                  rArgumentList = -(rArgument % ',');
                  rInvocation   = rIdentifier >> '(' >> rArgumentList >> ')';
                  rBlock        = '{' >> +rInvocation >> '}';
                  start         = qi::skip(qi::space) [ rBlock ];
      
                  BOOST_SPIRIT_DEBUG_NODES((start)(rBlock)(rInvocation)(rIdentifier)(rArgumentList)(rArgument)(rExpression))
              }
      
            private:
              qi::rule<Iterator, myAST::Block()> start;
              using Skipper = qi::space_type;
      
              qi::rule<Iterator, myAST::Argument(), Skipper>     rArgument;
              qi::rule<Iterator, myAST::ArgumentList(), Skipper> rArgumentList;
              qi::rule<Iterator, myAST::Invocation(), Skipper>   rInvocation;
              qi::rule<Iterator, myAST::Block(), Skipper>        rBlock;
              // implicit lexemes
              qi::rule<Iterator, myAST::Identifier()>   rIdentifier;
              qi::rule<Iterator, myAST::Expression()>   rExpression;
          };
      }
      
    6. Adding a test driver

      int main() {
          std::string const input = R"(
      {
          foo()
          bar(a, b, 42)
          qux(someThing_awful01 = 9)
      }
      )";
          auto f = input.data(), l = f + input.size();
      
          myAST::Block block;
          bool ok = parse(f, l, myNS::myRules<>{}, block);
      
          if (ok) {
              std::cout << "Parse success\n";
      
              for (auto& invocation : block) {
                  std::cout << invocation.identifier << "(";
                  for (auto& arg : invocation.args) std::cout << arg << ",";
                  std::cout << ")\n";
              }
          }
          else
              std::cout << "Parse failed\n";
      
          if (f!=l)
              std::cout << "Remaining unparsed input: '" << std::string(f,l) << "'\n";
      }
      

    Complete Demo

    See it Live On Coliru

    //#define BOOST_SPIRIT_DEBUG
    #include <boost/spirit/include/qi.hpp>
    
    namespace myAST {
        using Identifier = std::string;
        using Expression = boost::variant<char, int>;
    
        struct IdntifierEqArgument {
            Identifier ident;
            Expression arg;
        };
    
        using Argument = boost::variant<IdntifierEqArgument, Expression>;
    
        using ArgumentList = std::vector<Argument>;
    
        struct Invocation {
            Identifier identifier;
            ArgumentList args;
        };
    
        using Block = std::vector<Invocation>;
    
        // for debug printing
        static inline std::ostream& operator<<(std::ostream& os, myAST::IdntifierEqArgument const& named) {
            return os << named.ident << "=" << named.arg;
        }
    }
    
    BOOST_FUSION_ADAPT_STRUCT(myAST::IdntifierEqArgument, ident,arg);
    BOOST_FUSION_ADAPT_STRUCT(myAST::Invocation, identifier,args);
    
    namespace myNS {
        namespace qi = boost::spirit::qi;
    
        template <typename Iterator = char const*>
        struct myRules : qi::grammar<Iterator, myAST::Block()> {
    
            myRules() : myRules::base_type(start) {
                rIdentifier   = qi::raw [(qi::alpha | '_') >> *(qi::alnum | '_')];
                rExpression   = qi::alpha | qi::int_;
                rArgument     = (rIdentifier >> '=' >> rExpression) | rExpression;
                rArgumentList = -(rArgument % ',');
                rInvocation   = rIdentifier >> '(' >> rArgumentList >> ')';
                rBlock        = '{' >> +rInvocation >> '}';
                start         = qi::skip(qi::space) [ rBlock ];
    
                BOOST_SPIRIT_DEBUG_NODES((start)(rBlock)(rInvocation)(rIdentifier)(rArgumentList)(rArgument)(rExpression))
            }
    
          private:
            qi::rule<Iterator, myAST::Block()> start;
            using Skipper = qi::space_type;
    
            qi::rule<Iterator, myAST::Argument(), Skipper>     rArgument;
            qi::rule<Iterator, myAST::ArgumentList(), Skipper> rArgumentList;
            qi::rule<Iterator, myAST::Invocation(), Skipper>   rInvocation;
            qi::rule<Iterator, myAST::Block(), Skipper>        rBlock;
            // implicit lexemes
            qi::rule<Iterator, myAST::Identifier()>   rIdentifier;
            qi::rule<Iterator, myAST::Expression()>   rExpression;
        };
    }
    
    int main() {
        std::string const input = R"(
    {
        foo()
        bar(a, b, 42)
        qux(someThing_awful01 = 9)
    }
    )";
        auto f = input.data(), l = f + input.size();
    
        myAST::Block block;
        bool ok = parse(f, l, myNS::myRules<>{}, block);
    
        if (ok) {
            std::cout << "Parse success\n";
    
            for (auto& invocation : block) {
                std::cout << invocation.identifier << "(";
                for (auto& arg : invocation.args) std::cout << arg << ",";
                std::cout << ")\n";
            }
        }
        else
            std::cout << "Parse failed\n";
    
        if (f!=l)
            std::cout << "Remaining unparsed input: '" << std::string(f,l) << "'\n";
    }
    

    Prints output

    Parse success
    foo()
    bar(a,b,42,)
    qux(someThing_awful01=9,)
    Remaining unparsed input: '
    '