Spirit Qi attribute propagation issue with single-member struct
Asked Answered
Y

1

9

I have an compilation issue with Spirit Qi where it complains that value_type is not a member of identifier. For some reason, Qi's attribute system considers identifier to be a container type, and tries to enumerate it's value type.

This is a similar issue as in this question, however, I believe the cause is the single member struct and may be related to this bug.

#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>

using namespace boost::spirit::qi; 

struct identifier
{
    std::wstring name;
};

struct problem
{
    identifier _1;
    identifier _2;
    identifier _3;
};

BOOST_FUSION_ADAPT_STRUCT(
    identifier,
    (std::wstring, name)
)

BOOST_FUSION_ADAPT_STRUCT(
    problem,
    (identifier, _1)
    (identifier, _2)
    (identifier, _3)
)



int main(int argc, char* argv[])
{
    rule<std::wstring::const_iterator, identifier()> gr_identifier = eps >> raw[lexeme[(alpha | '_') >> *(alnum | '_')]];

    // Ok, compiles
    /*rule<std::wstring::const_iterator, problem()> gr_problem =       gr_identifier
                                                                  >> gr_identifier
                                                                  >> '('
                                                                  >>   gr_identifier
                                                                  >>   ')';*/
    // Fails
    rule<std::wstring::const_iterator, problem()> gr_problem =       gr_identifier
                                                                  >> gr_identifier
                                                                  >> '('
                                                                  >   gr_identifier
                                                                  >   ')';

    std::wstring input = L"foo goo(hoo)";
    /*bool dummy = phrase_parse(
            input.begin(), input.end(), 
            gr_problem,
            space);*/

    return EXIT_SUCCESS;
}

Intriguingly, this only occurs when using expectation parser (see definition 2 in the example). Definition 1, using only the sequence parser, compiles (and executes) properly.

Does anybody know the proper solution to this?

Please also check out the live example

Yellowwood answered 6/11, 2013 at 21:41 Comment(0)
A
15

This is a quite infamous edge-case in Spirit. The problem is, special-case handling of single-element Fusion Sequences in Spirit breaks some abstractions.

The usual workaround is to adapt the exposed-attribute side to be less-trivial:

rule<It, single_member_struct()> r = eps >> XXX; 
// the `eps` is there to break the spell

However, here that won't work, because your (a > XXX > b) subexpression results in another vector1<decltype(member_type)> and this time, no amount of smart parenthesizing or eps-ing will save you.[1]

To cut the long story short, I have three workarounds:


1. #define KEEP_STRING_WORKAROUND

See it Live On Coliru

In which you'll simply allow gr_identifier to return a std::wstring[2]:

rule<It, std::string()> gr_identifier = 
    (alpha | '_') >> *(alnum | '_');

This effectively just postpones the magic attribute transformation that uses the Fusion adaptation if identifier, and thereby breaks the spell:

rule<It, problem(), qi::space_type> gr_problem = 
       gr_identifier
    >> gr_identifier
    >> ('(' > gr_identifier > ')')
    ;

Just works. I think this is probably the least intrusive workaround


2. #define DUMMY_WORKAROUND

See it Live On Coliru

Whereby you unjinx the magix by ... making the identifier struct not fusion adapted to a single-element fusion sequence. Yes. This involves the EvilHack™ of adding a dummy field. To minimize confusion, lets' make it qi::unused_type though:

struct identifier
{
    std::string    name;
    qi::unused_type dummy;
};

BOOST_FUSION_ADAPT_STRUCT(
    identifier,
    (std::string,    name)
    (qi::unused_type, dummy)
)

And now:

rule<It, identifier()> gr_identifier = 
    (alpha | '_') >> *(alnum | '_') >> attr(42);   // that's hacky

Works


3. #define NO_ADAPT_WORKAROUND

See it Live On Coliru

The final workaround might be the most obvious: don't adapt the struct as a fusion sequence in the first place, and profit:

struct identifier
{
    std::string name;

    identifier() = default;

    explicit identifier(std::string name) 
        : name(std::move(name))
    {}
};

Note that to allow attribute propagation, now you will need suitable conversion constructors to be present. Also, the default constructor is required for exposed attributes in Spirit.

Now,

rule<It, identifier()> gr_identifier = 
    as_string [ (alpha | '_') >> *(alnum | '_') ]; // cleaner... but no fusion

works. This might be more intuitive if you didn't need Fusion of the type for other purposes.

Note: this variant could well be the most efficient in compile-time

Summary

I think for your code, there are 2 perfectly viable workarounds (#1 and #3), and one less-than-stellar (the one with the dummy field), but I included it for documentary purposes.

Full Code

For future reference

#define BOOST_SPIRIT_DEBUG
#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>

namespace qi = boost::spirit::qi;

//////////////////////////////////////////
// Select workaround to demonstrate
#define KEEP_STRING_WORKAROUND
// #define DUMMY_WORKAROUND™
// #define NO_ADAPT_WORKAROUND
//////////////////////////////////////////

#if defined(KEEP_STRING_WORKAROUND)
    struct identifier
    {
        std::string    name;
    };

    BOOST_FUSION_ADAPT_STRUCT(
        identifier,
        (std::string,    name)
    )
#elif defined(DUMMY_WORKAROUND)
    struct identifier
    {
        std::string    name;
        qi::unused_type dummy;
    };

    BOOST_FUSION_ADAPT_STRUCT(
        identifier,
        (std::string,    name)
        (qi::unused_type, dummy)
    )
#elif defined(NO_ADAPT_WORKAROUND)
    struct identifier
    {
        std::string name;

        identifier() = default;

        explicit identifier(std::string name) 
            : name(std::move(name))
        {}
    };
#endif

struct problem
{
    identifier _1;
    identifier _2;
    identifier _3;
};

BOOST_FUSION_ADAPT_STRUCT(
    problem,
    (identifier, _1)
    (identifier, _2)
    (identifier, _3)
)

//////////////////////////////////////////
// For BOOST_SPIRIT_DEBUG only:
static inline std::ostream& operator<<(std::ostream& os, identifier const& id) {
    return os << id.name;
}
//////////////////////////////////////////

int main()
{
    using namespace qi;
    typedef std::string::const_iterator It;
#if defined(KEEP_STRING_WORKAROUND)
    rule<It, std::string()> gr_identifier = 
        (alpha | '_') >> *(alnum | '_');
#elif defined(DUMMY_WORKAROUND)
    rule<It, identifier()> gr_identifier = 
        (alpha | '_') >> *(alnum | '_') >> attr(42);   // that's hacky
#elif defined(NO_ADAPT_WORKAROUND)
    rule<It, identifier()> gr_identifier = 
        as_string [ (alpha | '_') >> *(alnum | '_') ]; // cleaner... but no fusion
#endif

    rule<It, problem(), qi::space_type> gr_problem = 
           gr_identifier
        >> gr_identifier
        >> ('(' > gr_identifier > ')')
        ;

    std::string input = "foo goo(hoo)";

    BOOST_SPIRIT_DEBUG_NODES((gr_problem)(gr_identifier));

    It f(begin(input)), l(end(input));
    bool dummy = phrase_parse(f, l, gr_problem, qi::space);

    return dummy? 0 : 255;
}

[1] Believe me, I tried, even while inserting qi::unused_type "fake" attributes, and/or using attr_cast<> or helper rules to coerce the type of the subexpression.

[2] For demo purposes, I used std::string because I believe it mixes better with BOOST_SPIRIT_DEBUG

Astereognosis answered 6/11, 2013 at 22:44 Comment(5)
But what to do with single-fielded class, which field is a container? Say struct rvalue_list : tagged { std::deque< rvalue > rvalues_ };. Can I declare and define the c-tor like template< typename Iterator > rvalue_list::rvalue_list(Iterator const _first, Iterator const _last) : rvalues_(_first, _last) { ; }?Burin
@Dukales no? You'd make the constructor accept the exposed attributes. Anyways, here's the direct approach: coliru.stacked-crooked.com/a/d81411f836f7f97c - The alternative is to use custom traits to assign to your single-fielded class, which is then effectively a container: coliru.stacked-crooked.com/a/e455a29ade08042bAstereognosis
Seems like you have answered almost every boost related (IIRC at least spirit) question I had and will have. Greetings from the year 2018.Weise
How would the reverse i.e. karma way of NO_ADAPT_WORKAROUND look like? I couldn't figure it out with boost::spirit::karma::as, I guess because I would have to add a T/identifier constructor to std::vector<U/std::string> right? My current workaround is boost::spirit::karma::attr_cast with a coustom boost::spirit::traits::transform_attribute<T, std::vector<u>>{static std::vector<U> pre (const &T) functionWeise
@Weise I find that hard to answer without concrete code. Mind you, I practically don't use Karma. See e.g. #32579906 or #46839458 (both answers in both cases), or simply search Lounge<C++>Astereognosis

© 2022 - 2024 — McMap. All rights reserved.