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

Boost X3: Can a variant member be avoided in disjunctions?


I'd like to parse string | (string, int) and store it in a structure that defaults the int component to some value. The attribute of such a construction in X3 is a variant<string, tuple<string, int>>. I was thinking I could have a struct that takes either a string or a (string, int) to automagically be populated:

    struct bar
    {
        bar (std::string x = "", int y = 0) : baz1 {x}, baz2 {y} {}

        std::string          baz1;
        int                  baz2;
    };

BOOST_FUSION_ADAPT_STRUCT (disj::ast::bar, baz1, baz2)

and then simply have:

    const x3::rule<class bar, ast::bar> bar = "bar";
    using x3::int_;
    using x3::ascii::alnum;

    auto const bar_def = (+(alnum) | ('(' >> +(alnum) >> ',' >> int_ >> ')')) >> ';';

    BOOST_SPIRIT_DEFINE(bar);

However this does not work:

/usr/include/boost/spirit/home/x3/core/detail/parse_into_container.hpp:139:59: error: static assertion failed: Expecting a single element fusion sequence
  139 |             static_assert(traits::has_size<Attribute, 1>::value,

Setting baz2 to an optional does not help. One way to solve this is to have a variant field or inherit from that type:

    struct string_int {
        std::string s;
        int i;
    };

    struct foo {
        boost::variant<std::string, string_int>  var;
    };

BOOST_FUSION_ADAPT_STRUCT (disj::ast::string_int, s, i)
BOOST_FUSION_ADAPT_STRUCT (disj::ast::foo, var)

(For some reason, I have to use boost::variant instead of x3::variant for operator<< to work; also, using std::pair or tuple for string_int does not work, but boost::fusion::deque does.) One can then equip foo somehow to get the string and integer.

Question: What is the proper, clean way to do this in X3? Is there a more natural way than this second option and equipping foo with accessors?

Live On Coliru


Solution

  • Sadly the wording in the x3 section is exceedingly sparse and allows it (contrast the Qi section). A quick test confirms it:

    Live On Coliru

    #include <boost/spirit/home/x3.hpp>
    namespace x3 = boost::spirit::x3;
    
    template <typename Expr>
    std::string inspect(Expr const& expr) {
        using A = typename x3::traits::attribute_of<Expr, x3::unused_type>::type;
        return boost::core::demangle(typeid(A).name());
    }
    
    int main()
    {
        std::cout << inspect(x3::double_ | x3::int_) << "\n";       // variant expected
        std::cout << inspect(x3::int_ | "bla" >> x3::int_) << "\n"; // variant "understandable"
        std::cout << inspect(x3::int_ | x3::int_) << "\n";          // variant suprising:
    }
    

    Prints

    boost::variant<double, int>
    boost::variant<int, int>
    boost::variant<int, int>
    

    All Hope Is Not Lost

    In your specific case you could trick the system:

    auto const bar_def =                              //
        (+x3::alnum >> x3::attr(-1)                   //
        | '(' >> +x3::alnum >> ',' >> x3::int_ >> ')' //
        ) >> ';';
    

    Note how we "inject" an int value for the first branch. That satisfies the attribute propagation gods:

    Live On Coliru

    #include <boost/spirit/home/x3.hpp>
    #include <boost/fusion/adapted/struct.hpp>
    #include <boost/fusion/include/io.hpp>
    #include <iomanip>
    namespace x3 = boost::spirit::x3;
    
    namespace disj::ast {
        struct bar {
            std::string x;
            int         y;
        };
        using boost::fusion::operator<<;
    } // namespace disj::ast
    
    BOOST_FUSION_ADAPT_STRUCT(disj::ast::bar, x, y)
    
    namespace disj::parser {
        const x3::rule<class bar, ast::bar> bar = "bar";
    
        auto const bar_def =                              //
            (+x3::alnum >> x3::attr(-1)                   //
            | '(' >> +x3::alnum >> ',' >> x3::int_ >> ')' //
            ) >> ';';
    
        BOOST_SPIRIT_DEFINE(bar)
    }
    
    namespace disj {
        void run_tests() {
            for (std::string const input : {
                     "",
                     ";",
                     "bla;",
                     "bla, 42;",
                     "(bla, 42);",
                 }) {
    
                ast::bar val;
                auto f = begin(input), l = end(input);
    
                std::cout << "\n" << quoted(input) << " -> ";
    
                if (phrase_parse(f, l, parser::bar, x3::space, val)) {
                    std::cout << "Parsed: " << val << "\n";
                } else {
                    std::cout << "Failed\n";
                }
    
                if (f!=l) {
                    std::cout << " -- Remaining " << quoted(std::string_view(f, l)) << "\n";
                }
            }
        }
    }
    
    int main()
    {
        disj::run_tests();
    }
    

    Prints

    "" -> Failed
    
    ";" -> Failed
     -- Remaining ";"
    
    "bla;" -> Parsed: (bla -1)
    
    "bla, 42;" -> Failed
     -- Remaining "bla, 42;"
    
    "(bla, 42);" -> Parsed: (bla 42)
    

    ¹ just today