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

Boost::Spirit - Create class by semantic action (maybe by a C++ lambda?)


My goal is to parse something by boost::spirit, create a class (and maybe put it into a container) by using semantic actions.

I manage to create a class using boost::spirit using the BOOST_FUSION_ADAPT_STRUCT macro.

//The struct
using stringvec = std::string;
struct CVar
{
public:
    stringvec sVariable;
    CVar(stringvec sVariable) : sVariable(sVariable) {}
    CVar() {}
};
//The macro gluing everything
BOOST_FUSION_ADAPT_STRUCT(
    ::CVar,
    (stringvec, sVariable)
)
//The core of the grammar
varName = qi::char_("a-z");
start = *varName;
qi::rule<Iterator, std::string()> varName;
qi::rule<Iterator, std::vector<CVar>() > start;

http://coliru.stacked-crooked.com/a/56dd8325f854a8c9

Actually, I would have liked to omit the vector surrounding the class, but I ran into problems http://coliru.stacked-crooked.com/a/3719cee9c7594254 I thought I managed to match a class as the root of the tree at some point. The error message looks, like the adapt struct really wants to call push_back at some point. And somehow, it makes sense, in case there is no class to store.

However, I would like to call the constructor by a semantic action.

     start = (varName[qi::_val = boost::phoenix::construct<CVar>(qi::_1)]);

Coliru helped me a lot by printing a very direct error message (which I couldn't say about VSC++2022).\ error: no match for 'operator=' (operand types are 'std::vector' and 'CVar') http://coliru.stacked-crooked.com/a/7305e8483ee83b22 \

Furthermore, I would like to not use too much boost::bind or phoenix constructs. A plain lambda would be nice. I'm aware, that the [] operator already provides a kind of boost::lambda. However, I'm not sure how to use the placeholders qi::_val and qi::_1. Should I catch them in a lambda expression to execute something like qi::_val.push_back(Cvar(qi::_1))?

Some directions I looked into:

I would have expected something like this to work:

[&]() { qi::_val.push_back(CVar((std::string)qi::_1)) }

But how can I actually get it to work?

If it wasn't for my desire not to use boost and for the push_back, I could use boost::phoenix::construct( qi::_1), couldn't I?

I think this similar question is very important How to build a synthesized argument from a C++11 lambda semantic action in boost spirit? There I am still searching how to use the context to get access to qi::_val and qi::_1. It looks like I am on to something, but I already tried a lot and I might have to search for a tutorial how to read the Visual Studio errors or I should consider to switch the compiler.

P.S.: In VSC++2022 I also get two warnings which say something "boost::function_base::functor is not initialized and boost::detail::local_counted_base::count_type doesn't have range limits. Rank enum class before enum. How do I trigger them?


Solution

  • There's a lot that confuses me. You state "I want XYZ" without any reasoning why that would be preferable. Furthermore, you state the same for different approaches, so I don't know which one you prefer (and why).

    The example code define

    using stringvec = std::string;
    

    This is confusing, because stringvec as a name suggests vector<string>, not string? Right now it looks more like CVar is "like a string" and your attribute is vector<CVar>, i.e. like a vector of string, just not std::string.

    All in all I can give you the following hints:

    • in general, avoid semantic actions. They're heavy on the compiler, leaky abstractions, opt out of attribute compatibility¹, creates atomicity problems under backtracking (see Boost Spirit: "Semantic actions are evil"?)

    • secondly, if you use semantic actions, realize that the raw synthesized attribute for more parser expressions are Fusion sequences/containers.

      • In particular to get a std::string use qi::as_string[]. If you don't use semantic actions, indeed this kind of attribute compatibility/transformation is automatic¹.
      • in similar vein, to parse a single item into an explicit container, use repeat(1)[p]
    • the constructors work like you show with phx::construct<> except for all the downsides of relying on semantic actions

    Side observation: did you notice I reduced parser/AST friction in this previous answer by replacing std::string with char?

    Applied Answers:

    Q. Actually, I would have liked to omit the vector surrounding the class, but I ran into problems http://coliru.stacked-crooked.com/a/3719cee9c7594254 I thought I managed to match a class as the root of the tree at some point. The error message looks, like the adapt struct really wants to call push_back at some point. And somehow, it makes sense, in case there is no class to store.

    Simplified using as_string: Live On Coliru

    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    #include <iostream>
    namespace qi   = boost::spirit::qi;
    using Iterator = std::string::const_iterator;
    
    using stringval = std::string;
    struct CVar { stringval sVariable; };
    BOOST_FUSION_ADAPT_STRUCT(CVar, sVariable)
    
    struct TestGrammar : qi::grammar<Iterator, CVar()> {
        TestGrammar() : TestGrammar::base_type(start) {
            start = qi::as_string[qi::char_("a-z")];
        }
    
    private:
        qi::rule<Iterator, CVar() > start;
    };
    
    void do_test(std::string const& input) {
        CVar output;
    
        static const TestGrammar p;
    
        auto f = input.begin(), l = input.end();
        qi::parse(f, l, p, output);
    
        std::cout << std::quoted(input) << " -> " << std::quoted(output.sVariable) << "\n";
    }
    

    Using repeat(1): Live On Coliru

    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    #include <iostream>
    namespace qi   = boost::spirit::qi;
    using Iterator = std::string::const_iterator;
    
    using stringval = std::string;
    struct CVar { stringval sVariable; };
    BOOST_FUSION_ADAPT_STRUCT(CVar, sVariable)
    
    struct TestGrammar : qi::grammar<Iterator, CVar()> {
        TestGrammar() : TestGrammar::base_type(start) {
            cvar  = qi::repeat(1)[qi::char_("a-z")];
            start = cvar;
        }
    
      private:
        qi::rule<Iterator, CVar()> start;
        qi::rule<Iterator, std::string()> cvar;
    };
    
    void do_test(std::string const& input) {
        CVar output;
    
        static const TestGrammar p;
    
        auto f = input.begin(), l = input.end();
        qi::parse(f, l, p, output);
    
        std::cout << std::quoted(input) << " -> " << std::quoted(output.sVariable) << "\n";
    }
    

    Without using ADAPT_STRUCT: minimal change vs: simplified

    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    #include <iostream>
    namespace qi   = boost::spirit::qi;
    using Iterator = std::string::const_iterator;
    
    using stringval = std::string;
    struct CVar {
        CVar(std::string v = {}) : sVariable(std::move(v)) {}
        stringval sVariable;
    };
    
    struct TestGrammar : qi::grammar<Iterator, CVar()> {
        TestGrammar() : TestGrammar::base_type(start) {
            start = qi::as_string[qi::lower];
        }
    
      private:
        qi::rule<Iterator, CVar()> start;
    };
    

    Using Semantic Actions (not recommended): 4 modes live

    #include <boost/spirit/include/qi.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    #include <iomanip>
    #include <iostream>
    namespace qi   = boost::spirit::qi;
    namespace px   = boost::phoenix;
    using Iterator = std::string::const_iterator;
    
    using stringval = std::string;
    struct CVar {
        CVar(std::string v = {}) : sVariable(std::move(v)) {}
        stringval sVariable;
    };
    
    enum mode {
        AS_STRING_CONSTRUCT      = 1,
        DIRECT_ASSIGN            = 2,
        USING_ACTOR              = 3,
        TRANSPARENT_CXX14_LAMBDA = 4,
    };
    
    struct TestGrammar : qi::grammar<Iterator, CVar()> {
        TestGrammar(mode m) : TestGrammar::base_type(start) {
            switch (m) {
                case AS_STRING_CONSTRUCT: {
                        using namespace qi::labels;
                        start = qi::as_string[qi::lower][_val = px::construct<CVar>(_1)];
                        break;
                    }
                case DIRECT_ASSIGN: {
                        // or directly
                        using namespace qi::labels;
                        start = qi::lower[_val = px::construct<std::string>(1ull, _1)];
                        break;
                    }
                case USING_ACTOR: {
                        // or... indeed
                        using namespace qi::labels;
                        px::function as_cvar = [](char var) -> CVar { return {{var}}; };
                        start = qi::lower[_val = as_cvar(_1)];
                        break;
                    }
                    case TRANSPARENT_CXX14_LAMBDA: {
                        // or even more bespoke: (this doesn't require qi::labels or phoenix.hpp)
                        auto propagate = [](auto& attr, auto& ctx) {
                            at_c<0>(ctx.attributes) = {{attr}};
                        };
                        start          = qi::lower[propagate];
                        break;
                    }
            }
        }
    
      private:
        qi::rule<Iterator, CVar()> start;
    };
    
    void do_test(std::string const& input, mode m) {
        CVar output;
    
        const TestGrammar p(m);
    
        auto f = input.begin(), l = input.end();
        qi::parse(f, l, p, output);
    
        std::cout << std::quoted(input) << " -> " << std::quoted(output.sVariable) << "\n";
    }
    
    int main() {
        for (mode m : {AS_STRING_CONSTRUCT, DIRECT_ASSIGN, USING_ACTOR,
                       TRANSPARENT_CXX14_LAMBDA}) {
            std::cout << " ==== mode #" << static_cast<int>(m) << " === \n";
            for (auto s : {"a", "d", "ac"})
                do_test(s, m);
        }
    }
    

    Just to demonstrate how the latter two approaches can both do without the constructor or even without any phoenix support.

    As before, by that point I'd recommend going C++14 with Boost Spirit X3 anyways: http://coliru.stacked-crooked.com/a/dbd61823354ea8b6 or even 20 LoC: http://coliru.stacked-crooked.com/a/b26b3db6115c14d4


    ¹ there's a non-main "hack" that could help you there by defining BOOST_SPIRIT_ACTIONS_ALLOW_ATTR_COMPAT