boostboost-spiritboost-spirit-qi

how to map parse result with optional parts to a vector of variants?


i need to convert an old hierarchical query system parser to C++ my first idea was to port it 1:1 to C++ using Spirit

beware: most of the example-code on the bottom is my rule-test code - to adapt the old syntax, the rules are worked out and doing what i want and my test-queries getting parsed by the rules - nice!

but i have no idea how to port that to a qi:grammar based parser that spits out a vector of variants with the descent types i need from the query (with mandatory and partially optional parts)

can anyone give me an advice how to do the mapping? i think i need some BOOST_FUSION_ADAPT_STRUCT adaptors to fill my result-vector

the #if USE_GRAMMAR() code is not compilable - more or less a frame for your tips

// #define BOOST_SPIRIT_DEBUG
#include <boost/fusion/include/std_pair.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iomanip>
#include <map>
#include <optional>
#include <variant>

namespace qi = boost::spirit::qi;

// these are my result-variant types

struct Member{ std::string name; };
struct Subscription{ std::size_t indice; };
using Class_list = std::vector<std::string>;

struct Instance_id{
    std::string class_name;
    std::string instance_name;
};
using Instance_list = std::vector<Instance_id>;

struct Deep_search{ std::optional<int> max_depth; };

struct Property_filter{};
struct Parent{};
struct Self{};
struct Child{};

using Value = std::variant<Member, Class_list, Instance_list, Deep_search, Subscription, Property_filter, Parent, Self, Child>;
using Result = std::vector<Value>;

// code that should use the qi::grammar parser
// set that true and get a bunch of compile-errors :)
#define USE_GRAMMAR() ( false )

#if USE_GRAMMAR()
template <typename It>
struct Query_parser : qi::grammar<It, Result()>
{
    Query_parser() : Query_parser::base_type( start ){
        using namespace qi;

        start = skip( space )[*query];

        // copy of the test rules from bottom
        identifier = qi::char_( "a-zA-Z_" ) >> *qi::char_( "a-zA-Z0-9_" );
        subscription = '[' >> qi::uint_ >> ']';
        member = identifier >> -subscription;
        class_list = '%' >> ( identifier | '(' >> -( identifier % ',' ) >> ')' );
        instance_id = identifier >> ':' >> identifier;
        instance_list = qi::skip( qi::blank )['#' >> ( instance_id | '(' >> -( instance_id % ',' ) >> ')' )];
        instance_or_class_list = class_list | instance_list;
        instance_or_class_list_filter = instance_or_class_list >> -subscription;
        property_filter = qi::char_( "!" ) >> '(' >> ')';
        deep_search = "?" >> -( '(' >> qi::uint_ >> ')' ) >> instance_or_class_list >> -property_filter >> -subscription;
        parent = "..";
        self = ".";
        child = "/";
        query_part = ( member | child | parent | self | deep_search | instance_or_class_list_filter );
        query = query_part >> *( child >> *query_part );
    }

private:
    qi::rule<It, Result()> start;
    // i don't know how to map the values in to the Result vector
    qi::rule<It, char const*> identifier; // -> only a rule build helper
    qi::rule<It, char const*> subscription; // output
    qi::rule<It, char const*> member; // output
    qi::rule<It, char const*> class_list; // output
    qi::rule<It, char const*> instance_id;
    qi::rule<It, char const*> instance_list; // output
    qi::rule<It, char const*> instance_or_class_list;
    qi::rule<It, char const*> instance_or_class_list_filter;
    qi::rule<It, char const*> property_filter; // output
    qi::rule<It, char const*> deep_search; // output
    qi::rule<It, char const*> parent; // output
    qi::rule<It, char const*> self; // output
    qi::rule<It, char const*> child; // output
    qi::rule<It, char const*> query; // output
    qi::rule<It, char const*> query_part; // -> only a rule build helper
};
#endif

template <typename P>
bool test_parser( char const* input, P const& p, bool full_match = true ){
    using boost::spirit::qi::parse;
    char const* f( input );
    char const* l( f + strlen( f ) );
    return parse( f, l, p ) && ( !full_match || ( f == l ) );
}

int main()
{
#if USE_GRAMMAR()
    using It = std::string::const_iterator;
    Query_parser<It> const p;

    for( std::string const& input : { "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A" } ){
        It f = input.begin(), l = input.end();
        Result data;
        bool ok = parse( f, l, p, data );

        if( ok ){
            std::cout << "Parse o\n";
        }
        else{
            std::cout << "Parse failed\n";
        }

        if( f != l )
            std::cout << "Remaining unparsed: " << std::quoted( std::string( f, l ) ) << "\n";
    }

    int brk = 1;

#else
    // my working rule tests to get a feeling what i try to reach

    // identifier
    qi::rule<char const*> identifier;
    identifier = qi::char_( "a-zA-Z_" ) >> *qi::char_( "a-zA-Z0-9_" );
    assert( test_parser( "_a_b4", identifier ) );

    // subcription
    qi::rule<char const*> subscription;
    subscription = '[' >> qi::uint_ >> ']';
    assert( test_parser( "[2]", subscription ) );
    assert( test_parser( "[45]", subscription ) );

    // member
    qi::rule<char const*> member;
    member = identifier >> -subscription;
    assert( test_parser( "abc", member ) );
    assert( test_parser( "abc[2]", member ) );

    // class list
    qi::rule<char const*> class_list;
    class_list = '%' >> ( identifier | '(' >> -( identifier % ',' ) >> ')' );
    assert( test_parser( "%A", class_list ) );       // vector<{{class_name}}> size 1
    assert( test_parser( "%(A,B,C)", class_list ) ); // vector<{{class_name},{},...}> size n

    // instance id
    qi::rule<char const*> instance_id;
    instance_id = identifier >> ':' >> identifier;
    assert( test_parser( "_a_b4:blub", instance_id ) );

    // instance list
    qi::rule<char const*> instance_list;
    instance_list = qi::skip( qi::blank )['#' >> ( instance_id | '(' >> -( instance_id % ',' ) >> ')' )];
    assert( test_parser( "#a:A", instance_list ) );       // vector<{{class_name, instance_name}}> size 1
    assert( test_parser( "#(a:A,b:B)", instance_list ) ); // vector<{{class_name, instance_name},{},...}> size n

    // combined class/instance-list
    qi::rule<char const*> instance_or_class_list;
    instance_or_class_list = class_list | instance_list;
    assert( test_parser( "#(a:A,b:B)", instance_or_class_list ) );
    assert( test_parser( "%(A,B,C)", instance_or_class_list ) );

    qi::rule<char const*> instance_or_class_list_filter;
    instance_or_class_list_filter = instance_or_class_list >> -subscription;
    assert( test_parser( "#(a:A,b:B)[2]", instance_or_class_list_filter ) );
    assert( test_parser( "%(A,B,C)[5]", instance_or_class_list_filter ) );

    // expression - dummy
    qi::rule<char const*> property_filter;
    property_filter = qi::char_( "!" ) >> '(' >> ')';

    // deep search
    qi::rule<char const*> deep_search;
    deep_search = "?" >> -( '(' >> qi::uint_ >> ')' ) >> instance_or_class_list >> -property_filter >> -subscription;
    assert( test_parser( "?%ABC", deep_search ) );
    assert( test_parser( "?%ABC[2]", deep_search ) );
    assert( test_parser( "?%(A,B,C)", deep_search ) );
    assert( test_parser( "?%(A,B,C)[2]", deep_search ) );
    assert( test_parser( "?#ABC:EFG", deep_search ) );
    assert( test_parser( "?#(A:a,B:b,C:c)", deep_search ) );
    assert( test_parser( "?(2)%blub!()[2]", deep_search ) );

    // goto parent
    qi::rule<char const*> parent;
    parent = "..";
    assert( test_parser( "..", parent ) );

    // stay
    qi::rule<char const*> self; // um im Root zu suchen
    self = ".";
    assert( test_parser( ".", self ) );

    // member or root
    qi::rule<char const*> child; // or root
    child = "/";
    assert( test_parser( "/", child ) );

    // complete query
    qi::rule<char const*> query;
    qi::rule<char const*> query_part;
    query_part = ( member | child | parent | self | deep_search | instance_or_class_list_filter );
    query = query_part >> *( child >> *query_part );

    assert( test_parser( "#(A:a,B:b)[2]", query ) );
    assert( test_parser( "abc/..", query ) );
    assert( test_parser( "abc/.", query ) );
    assert( test_parser( "..", query ) );
    assert( test_parser( ".", query ) );
    assert( test_parser( "../", query ) );
    assert( test_parser( "./", query ) );
    assert( test_parser( "abc", query ) );
    assert( test_parser( "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A", query ) );
    assert( test_parser( "abc[2]", query ) );
    assert( test_parser( "/", query ) );
    assert( test_parser( "?(2)%ABC", query ) );
    assert( test_parser( "?(2)%(ABC)!()[2]", query ) );
    assert( test_parser( "abc[2]", query ) );
    assert( test_parser( "abc/?(2)%Class[2]", query ) );
    assert( test_parser( "?(2)%ABC", query ) );

    /*
        "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A"
        
        should give a Result Value vector of

        {
            Member, 
            Child, 
            Parent, 
            Child, 
            Member, 
            Child, 
            Member,
            Subcription, 
            Child
            Deep_search, 
            Instance_list, 
            Property_filter, 
            Subscription, 
            Child
            Member, 
            Subcription,
            Child, 
            Member, 
            Child, 
            Deep_search,
            Class_list  
        }
    */

#endif

    return 0;
}

thanks for any advice

UPDATE

i tried to extend sehe's example replacing the Query with a struct with rooted bool and a path parts vector

# changed Ast::Query
using Parts = std::vector<Part>;
struct Query
{
    bool rooted{};
    Parts parts;
};

# another adapter
BOOST_FUSION_ADAPT_STRUCT( AST::Query, rooted, parts )

# two new rules
qi::rule<It, AST::Parts()> parts;
qi::rule<It, bool()> rooted;

# rule definition
parts           = part % '/';
rooted          = qi::matches["/"];
query           = rooted >> parts;

and changed the debug printers accordingly but i always get a compilation error somewhere in boost\boost\spirit\home\support\container.hpp

//[customization_container_value_default
template <typename Container, typename Enable/* = void*/>
struct container_value
  : detail::remove_value_const<typename Container::value_type>
{};

Live On Coliru (if the execution time doesn't exceed the limit)


Solution

  • The grammar you show is more of a list of token definitions. The process you're implementing, then, becomes lexing (or token scanning), not parsing.

    In your grammar, all your rules (except start) are defined as

    qi::rule<It, char const*>
    

    Which evaluates to

    qi::rule<std::string::const_iterator, char const*>
    

    This is an allowed shorthand for

    qi::rule<std::string::const_iterator, char const*()>
    

    Meaning that the synthesized attribute is also char const*. That seems not what you meant, but we have to guess what you thought it would do.

    Also note that none of these rules declare a skipper, so the toplevel skip(space) in start has very little effect (except pre-skipping before the first query element). See Boost spirit skipper issues

    First Step

    As a first step I removed some of the previously mentioned problems, introduced an AST namespace (which is the more general name for your "result variant"), added rule debugging and made some consistency fixes.

    At least it now compiles and runs: Live On Coliru

    // #define BOOST_SPIRIT_DEBUG
    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/fusion/include/std_pair.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    #include <map>
    #include <optional>
    #include <variant>
    
    namespace qi = boost::spirit::qi;
    
    // my AST types
    namespace AST {
        struct Member    { std::string name;   };
        struct Subscript { std::size_t indice; };
        using Class_list = std::vector<std::string>;
    
        struct Instance_id {
            std::string class_name;
            std::string instance_name;
        };
    
        using Instance_list = std::vector<Instance_id>;
    
        struct Deep_search{ std::optional<int> max_depth; };
    
        struct Property_filter {};
        struct Parent {};
        struct Self {};
        struct Child {};
    
        using Value  = std::variant< //
            Member,                 //
            Class_list,             //
            Instance_list,          //
            Deep_search,            //
            Subscript,              //
            Property_filter,        //
            Parent,                 //
            Self,                   //
            Child>;
        using Result = std::vector<Value>;
    } // namespace AST
    
    template <typename It> struct Query_parser : qi::grammar<It, AST::Result()> {
        Query_parser() : Query_parser::base_type(start) {
    
            start = qi::skip(qi::space)[*query];
    
            // copy of the test rules from bottom
            identifier =
                qi::lexeme[qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z0-9_")];
            subscript     = '[' >> qi::uint_ >> ']';
            member        = identifier >> -subscript;
            class_list    = '%' >> (identifier | '(' >> -(identifier % ',') >> ')');
            instance_id   = identifier >> ':' >> identifier;
            instance_list = qi::skip(qi::blank) //
                ['#' >> (instance_id | '(' >> -(instance_id % ',') >> ')')];
            instance_or_class_list        = class_list | instance_list;
            instance_or_class_list_filter = instance_or_class_list >> -subscript;
            property_filter               = qi::char_("!") >> '(' >> ')';
            deep_search                   = "?" >> -('(' >> qi::uint_ >> ')') >>
                instance_or_class_list >> -property_filter >> -subscript;
            parent     = "..";
            self       = ".";
            separator  = "/";
            query_part = (member | separator | parent | self | deep_search |
                          instance_or_class_list_filter);
            query      = query_part >> *(separator >> *query_part);
    
            BOOST_SPIRIT_DEBUG_NODES((start)(identifier)(subscript)(member)(
                class_list)(instance_id)(instance_list)(instance_or_class_list)(
                instance_or_class_list_filter)(property_filter)(deep_search)(
                parent)(self)(separator)(query_part)(query))
        }
    
      private:
        qi::rule<It, AST::Result()> start;
        // i don't know how to map the values in to the Result vector
    
        qi::rule<It> identifier; // -> only a rule build helper
        qi::rule<It> subscript;  // output
        qi::rule<It> member;     // output
        qi::rule<It> class_list; // output
        qi::rule<It> instance_id;
        qi::rule<It> instance_list; // output
        qi::rule<It> instance_or_class_list;
        qi::rule<It> instance_or_class_list_filter;
        qi::rule<It> property_filter; // output
        qi::rule<It> deep_search;     // output
        qi::rule<It> parent;          // output
        qi::rule<It> self;            // output
        qi::rule<It> separator;       // output
        qi::rule<It> query;           // output
        qi::rule<It> query_part;      // -> only a rule build helper
    };
    
    template <typename P>
    bool test_parser(std::string const& input, P const& p) {
        auto f(begin(input)), l(end(input));
        return parse(f, l, p >> qi::eoi);
    }
    
    int main()
    {
        static const std::string SAMPLE =
            "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A";
    
        using It = std::string::const_iterator;
    #if 1
        Query_parser<It> const p;
    
        for (std::string const& input :
           {SAMPLE}) {
            It          f = input.begin(), l = input.end();
            AST::Result data;
    
            if (/*bool ok =*/parse(f, l, p, data)) {
                std::cout << "Parse ok\n";
            } else {
                std::cout << "Parse failed\n";
            }
    
            if( f != l )
                std::cout << "Remaining unparsed: " << std::quoted( std::string( f, l ) ) << "\n";
        }
    #endif
    
    #if 1
        // my working rule tests to get a feeling what i try to reach
        // identifier
        qi::rule<It> identifier;
        identifier = qi::char_( "a-zA-Z_" ) >> *qi::char_( "a-zA-Z0-9_" );
        assert(test_parser("_a_b4", identifier));
    
        // subcription
        qi::rule<It> subscription;
        subscription = '[' >> qi::uint_ >> ']';
        assert(test_parser("[2]", subscription));
        assert(test_parser("[45]", subscription));
    
        // member
        qi::rule<It> member;
        member = identifier >> -subscription;
        assert(test_parser("abc", member));
        assert(test_parser("abc[2]", member));
    
        // class list
        qi::rule<It> class_list;
        class_list = '%' >> (identifier | '(' >> -(identifier % ',') >> ')');
        assert(test_parser("%A", class_list)); // vector<{{class_name}}> size 1
        assert(test_parser("%(A,B,C)",
                           class_list)); // vector<{{class_name},{},...}> size n
    
        // instance id
        qi::rule<It> instance_id;
        instance_id = identifier >> ':' >> identifier;
        assert(test_parser("_a_b4:blub", instance_id));
    
        // instance list
        qi::rule<It> instance_list;
        instance_list = qi::skip( qi::blank )['#' >> (instance_id | '(' >> -(instance_id % ',') >> ')')];
        assert(test_parser(
            "#a:A", instance_list)); // vector<{{class_name, instance_name}}> size 1
        assert(test_parser(
            "#(a:A,b:B)",
            instance_list)); // vector<{{class_name, instance_name},{},...}> size n
    
        // combined class/instance-list
        qi::rule<It> instance_or_class_list;
        instance_or_class_list = class_list | instance_list;
        assert(test_parser("#(a:A,b:B)", instance_or_class_list));
        assert(test_parser("%(A,B,C)", instance_or_class_list));
    
        qi::rule<It> instance_or_class_list_filter;
        instance_or_class_list_filter = instance_or_class_list >> -subscription;
        assert(test_parser("#(a:A,b:B)[2]", instance_or_class_list_filter));
        assert(test_parser("%(A,B,C)[5]", instance_or_class_list_filter));
    
        // expression - dummy
        qi::rule<It> property_filter;
        property_filter = qi::char_( "!" ) >> '(' >> ')';
    
        // deep search
        qi::rule<It> deep_search;
        deep_search = "?" >> -( '(' >> qi::uint_ >> ')' ) >> instance_or_class_list >> -property_filter >> -subscription;
        assert(test_parser("?%ABC", deep_search));
        assert(test_parser("?%ABC[2]", deep_search));
        assert(test_parser("?%(A,B,C)", deep_search));
        assert(test_parser("?%(A,B,C)[2]", deep_search));
        assert(test_parser("?#ABC:EFG", deep_search));
        assert(test_parser("?#(A:a,B:b,C:c)", deep_search));
        assert(test_parser("?(2)%blub!()[2]", deep_search));
    
        // goto parent
        qi::rule<It> parent;
        parent = "..";
        assert(test_parser("..", parent));
    
        // stay
        qi::rule<It> self; // um im Root zu suchen
        self = ".";
        assert(test_parser(".", self));
    
        // member or root
        qi::rule<It> child; // or root
        child = "/";
        assert(test_parser("/", child));
    
        // complete query
        qi::rule<It> query;
        qi::rule<It> query_part;
        query_part = (member | child | parent | self | deep_search |
                      instance_or_class_list_filter);
        query      = query_part >> *(child >> *query_part);
    
        assert(test_parser("#(A:a,B:b)[2]",     query));
        assert(test_parser("abc/..",            query));
        assert(test_parser("abc/.",             query));
        assert(test_parser("..",                query));
        assert(test_parser(".",                 query));
        assert(test_parser("../",               query));
        assert(test_parser("./",                query));
        assert(test_parser("abc",               query));
        assert(test_parser(SAMPLE,              query));
        assert(test_parser("abc[2]",            query));
        assert(test_parser("/",                 query));
        assert(test_parser("?(2)%ABC",          query));
        assert(test_parser("?(2)%(ABC)!()[2]",  query));
        assert(test_parser("abc[2]",            query));
        assert(test_parser("abc/?(2)%Class[2]", query));
        assert(test_parser("?(2)%ABC",          query));
    
        /*
            SAMPLE should give a Result Value vector of
    
            {
                Member,
                Child,
                Parent,
                Child,
                Member,
                Child,
                Member,
                Subcription,
                Child
                Deep_search,
                Instance_list,
                Property_filter,
                Subscription,
                Child
                Member,
                Subcription,
                Child,
                Member,
                Child,
                Deep_search,
                Class_list
            }
        */
    #endif
    }
    

    Prints (with all asserts passing):

    Parse ok
    

    AST

    You will want to map your rules to synthesized AST nodes. Your approach, going by input token, doesn't map nicely on the qi::rule model because it assumes "global" access to an output token stream. While you can achieve that, what qi::rule naturally wants to do is synthesize attributes which are returned by value and then be composed into higher level AST nodes.

    Using my imagination I'd say you want something more structured. Notably, I see that member, class_list and instance_list can be followed by an optional subscript. I'd express that logically:

     filterable = member | class_list | instance_list;
     filter     = filterable >> -subscript;
    

    To be honest it looks like the grammar would become more consistent with:

     filter     = filterable >> property_filter >> -subscript;
    

    Caveat: my ability to guess useful names in AST representation is severely limit by a complete lack of context (e.g. what is -('(' >> qi::uint_ >> ')') in deep_search? I'll just call it count and default it to 1. This might not make sense with the actual meaning.

    As a first shot, this would structurally simply the rules to:

    identifier      = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z0-9_");
    member          = identifier;
    class_list      = '%' >> (identifier | '(' >> -(identifier % ',') >> ')');
    instance_id     = identifier >> ':' >> identifier;
    instance_list   = '#' >> (instance_id | '(' >> -(instance_id % ',') >> ')');
    subscript       = '[' >> qi::uint_ >> ']';
    property_filter = qi::matches["!()"];
    
    filterable      = member | class_list | instance_list;
    filtered        = filterable >> property_filter  >> -subscript;
    
    max_depth       = '(' >> qi::uint_ >> ')' | qi::attr(-1);
    deep_search     = "?" >> max_depth >> filtered;
    parent          = "..";
    self            = ".";
    part            = parent | self | deep_search | filtered;
    query           = part % '/';
    

    I hope you can appreciate the reduced complexity.

    Now let's create matching AST nodes:

    using Identifier = std::string;
    using Member     = Identifier;
    using Class_list = std::vector<Identifier>;
    using Index      = unsigned;
    
    struct Instance_id { Identifier class_name, instance_name; };
    using Class_list    = std::vector<Identifier>;
    using Instance_list = std::vector<Instance_id>;
    using Filterable    = std::variant<Member, Class_list, Instance_list>;
    
    struct Filter {
        Filterable           subject;
        bool                 prop_filter;
        std::optional<Index> subscript;
    };
    
    struct DeepSearch {
        std::optional<int> max_depth;
        Filter             expr;
    };
    
    struct Parent {};
    struct Self {};
    
    using Part  = std::variant<Filter, DeepSearch, Parent, Self>;
    using Query = std::vector<Part>;
    

    Now, just matching the rule declarations:

    qi::rule<It, AST::Identifier()>    identifier;
    qi::rule<It, AST::Index()>         subscript;
    qi::rule<It, AST::Member()>        member;
    qi::rule<It, AST::Class_list()>    class_list;
    qi::rule<It, AST::Instance_id()>   instance_id;
    
    qi::rule<It, AST::Filterable()>    filterable;
    qi::rule<It, AST::Filter()>        filter;
    
    qi::rule<It, AST::Instance_list()> instance_list;
    qi::rule<It, bool()>               property_filter;
    qi::rule<It, int>                  max_depth;
    qi::rule<It, AST::DeepSearch()>    deep_search;
    qi::rule<It, AST::Parent()>        parent;
    qi::rule<It, AST::Self()>          self;
    qi::rule<It, AST::Part()>          part;
    qi::rule<It, AST::Query()>         query;
    

    And sprinkling a minimal amount of glue:

    parent  = ".." >> qi::attr(AST::Parent{});
    self    = "." >> qi::attr(AST::Self{});
    

    Already makes it compile in c++20: Live On Coliru.

    Instead adding fusion adaptation for c++17: Live On Coliru

    BOOST_FUSION_ADAPT_STRUCT(AST::InstanceId, class_name, instance_name)
    BOOST_FUSION_ADAPT_STRUCT(AST::Filter, subject, prop_filter, subscript)
    BOOST_FUSION_ADAPT_STRUCT(AST::DeepSearch, max_depth, expr)
    BOOST_FUSION_ADAPT_STRUCT(AST::Parent)
    BOOST_FUSION_ADAPT_STRUCT(AST::Self)
    

    I'll stick with the Fusion (c++17 compatible) approach.

    Debug Output

    Because laziness is a virtue, I changed to boost::variant. I've integrated all the "minor" test cases in the main Qi parsing loop. Some of these tests don't look like they're supposed to match the query production, so they will croak:

    Live On Coliru

    //#define BOOST_SPIRIT_DEBUG
    #include <boost/fusion/include/adapt_struct.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <iomanip>
    #include <optional>
    
    namespace qi = boost::spirit::qi;
    
    // my AST types
    namespace AST {
        struct Identifier : std::string {
            using std::string::string;
            using std::string::operator=;
        };
        using Member     = Identifier;
        using Class_list = std::vector<Identifier>;
        using Index      = unsigned;
    
        struct InstanceId { Identifier class_name, instance_name; };
        using Class_list    = std::vector<Identifier>;
        using Instance_list = std::vector<InstanceId>;
        using Filterable    = boost::variant<Member, Class_list, Instance_list>;
    
        struct Filter {
            Filterable           subject;
            bool                 prop_filter;
            std::optional<Index> subscript;
        };
    
        struct DeepSearch {
            int    max_depth;
            Filter expr;
        };
    
        struct Parent {};
        struct Self {};
    
        using Part  = boost::variant<Filter, DeepSearch, Parent, Self>;
        using Query = std::vector<Part>;
    } // namespace AST
    
    BOOST_FUSION_ADAPT_STRUCT(AST::InstanceId, class_name, instance_name)
    BOOST_FUSION_ADAPT_STRUCT(AST::Filter, subject, prop_filter, subscript)
    BOOST_FUSION_ADAPT_STRUCT(AST::DeepSearch, max_depth, expr)
    BOOST_FUSION_ADAPT_STRUCT(AST::Parent)
    BOOST_FUSION_ADAPT_STRUCT(AST::Self)
    
    namespace AST {
        std::ostream& operator<<(std::ostream& os, Parent) { return os << ".."; }
        std::ostream& operator<<(std::ostream& os, Self) { return os << "."; }
        std::ostream& operator<<(std::ostream& os, InstanceId const& iid) {
            return os << iid.class_name << ":" << iid.instance_name;
        }
        std::ostream& operator<<(std::ostream& os, Filter const& f) {
            os << f.subject;
            if (f.prop_filter) os << "!()";
            if (f.subscript)    os << '[' << *f.subscript << ']';
            return os;
        }
        std::ostream& operator<<(std::ostream& os, DeepSearch const& ds) {
            os << "?";
            if (ds.max_depth != -1)
                os << "(" << ds.max_depth << ")";
            return os << ds.expr;
        }
        std::ostream& operator<<(std::ostream& os, Class_list const& cl) {
            bool first = true;
            os << "%(";
            for (auto& id : cl) {
                os << (std::exchange(first,false)?"":",") << id;
            }
            return os << ")";
        }
        std::ostream& operator<<(std::ostream& os, Query const& q) {
            bool first = true;
            for (auto& p : q) {
                os << (std::exchange(first,false)?"":"/") << p;
            }
            return os;
        }
        std::ostream& operator<<(std::ostream& os, Instance_list const& il) {
            bool first = true;
            os << "#(";
            for (auto& iid : il) {
                os << (std::exchange(first,false)?"":",") << iid;
            }
            return os << ")";
        }
    } // namespace AST
    
    template <typename It> struct QueryParser : qi::grammar<It, AST::Query()> {
        QueryParser() : QueryParser::base_type(start) {
            start = qi::skip(qi::space)[*query];
    
            identifier      = qi::char_("a-zA-Z_") >> *qi::char_("a-zA-Z0-9_");
            member          = identifier;
            class_list      = '%' >> (identifier | '(' >> -(identifier % ',') >> ')');
            instance_id     = identifier >> ':' >> identifier;
            instance_list   = '#' >> (instance_id | '(' >> -(instance_id % ',') >> ')');
            subscript       = '[' >> qi::uint_ >> ']';
            property_filter = qi::matches["!()"]; // TODO maybe reintroduce a skipper?
    
            filterable      = member | class_list | instance_list;
            filter          = filterable >> property_filter >> -subscript;
    
            max_depth       = '(' >> qi::uint_ >> ')' | qi::attr(-1);
            deep_search     = "?" >> max_depth >> filter;
            parent          = ".." >> qi::attr(AST::Parent{});
            self            = "." >> qi::attr(AST::Self{});
            part            = parent | self | deep_search | filter;
            query           = part % '/';
    
            BOOST_SPIRIT_DEBUG_NODES((start)(identifier)(subscript)(member)(
                class_list)(instance_id)(instance_list)(property_filter)(max_depth)(
                filterable)(filter)(deep_search)(parent)(self)(part)(query))
        }
    
      private:
        qi::rule<It, AST::Query()> start;
    
        qi::rule<It, AST::Identifier()>    identifier;
        qi::rule<It, AST::Index()>         subscript;
        qi::rule<It, AST::Member()>        member;
        qi::rule<It, AST::Class_list()>    class_list;
        qi::rule<It, AST::InstanceId()>   instance_id;
    
        qi::rule<It, AST::Filterable()>    filterable;
        qi::rule<It, AST::Filter()>        filter;
    
        qi::rule<It, AST::Instance_list()> instance_list;
        qi::rule<It, bool()>               property_filter;
        qi::rule<It, int>                  max_depth;
        qi::rule<It, AST::DeepSearch()>    deep_search;
        qi::rule<It, AST::Parent()>        parent;
        qi::rule<It, AST::Self()>          self;
        qi::rule<It, AST::Part()>          part;
        qi::rule<It, AST::Query()>         query;
    };
    
    int main()
    {
        using It = std::string::const_iterator;
        QueryParser<It> const p;
    
        for (std::string const input :
             {
                 "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A",
                 "?(2)%ABC",
                 "abc/?(2)%Class[2]",
                 "abc[2]",
                 "?(2)%(ABC)!()[2]",
                 "?(2)%ABC",
                 "/",
                 "abc[2]",
                 "abc",
                 "./",
                 "../",
                 ".",
                 "..",
                 "abc/.",
                 "abc/..",
                 "#(A:a,B:b)[2]",
                 ".",
                 "..",
                 "?(2)%blub!()[2]",
                 "?#(A:a,B:b,C:c)",
                 "?#ABC:EFG",
                 "?%(A,B,C)[2]",
                 "?%(A,B,C)",
                 "?%ABC[2]",
                 "?%ABC",
                 "%(A,B,C)[5]",
                 "#(a:A,b:B)[2]",
                 "%(A,B,C)",
                 "#(a:A,b:B)",
    
                 "_a_b4:blub",
                 "%(A,B,C)",
                 "%A",
                 "abc[2]",
                 "abc",
                 "[45]",
                 "[2]",
                 "_a_b4",
             }) //
        {
            It         f = input.begin(), l = input.end();
            AST::Query parsed_query;
            std::cout << "=== " << std::quoted(input) << " ===\n";
    
            if (parse(f, l, p /*>> qi::eoi*/, parsed_query)) {
                std::cout << "Parsed: " << parsed_query << "\n";
                for (size_t i = 0; i < parsed_query.size(); ++i) {
                    auto& part = parsed_query[i];
                    std::cout << " - at #" << i << " part of type " //
                              << std::setw(17) << std::left
                              << boost::core::demangle(part.type().name()) + ": "
                              << part << "\n";
                }
            } else {
                std::cout << "Parse failed\n";
            }
    
            if (f != l)
                std::cout << "Remaining unparsed: "
                          << std::quoted(std::string(f, l)) << "\n";
        }
    }
    

    Prints

    === "ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%A" ===
    Parsed: ube/../abc/abc[2]/?(3)#(A:a,B:b)!()[2]/blub[2]/test/?%(A)
     - at #0 part of type AST::Filter:     ube
     - at #1 part of type AST::Parent:     ..
     - at #2 part of type AST::Filter:     abc
     - at #3 part of type AST::Filter:     abc[2]
     - at #4 part of type AST::DeepSearch: ?(3)#(A:a,B:b)!()[2]
     - at #5 part of type AST::Filter:     blub[2]
     - at #6 part of type AST::Filter:     test
     - at #7 part of type AST::DeepSearch: ?%(A)
    === "?(2)%ABC" ===
    Parsed: ?(2)%(ABC)
     - at #0 part of type AST::DeepSearch: ?(2)%(ABC)
    === "abc/?(2)%Class[2]" ===
    Parsed: abc/?(2)%(Class)[2]
     - at #0 part of type AST::Filter:     abc
     - at #1 part of type AST::DeepSearch: ?(2)%(Class)[2]
    === "abc[2]" ===
    Parsed: abc[2]
     - at #0 part of type AST::Filter:     abc[2]
    === "?(2)%(ABC)!()[2]" ===
    Parsed: ?(2)%(ABC)!()[2]
     - at #0 part of type AST::DeepSearch: ?(2)%(ABC)!()[2]
    === "?(2)%ABC" ===
    Parsed: ?(2)%(ABC)
     - at #0 part of type AST::DeepSearch: ?(2)%(ABC)
    === "/" ===
    Parsed: 
    Remaining unparsed: "/"
    === "abc[2]" ===
    Parsed: abc[2]
     - at #0 part of type AST::Filter:     abc[2]
    === "abc" ===
    Parsed: abc
     - at #0 part of type AST::Filter:     abc
    === "./" ===
    Parsed: .
     - at #0 part of type AST::Self:       .
    Remaining unparsed: "/"
    === "../" ===
    Parsed: ..
     - at #0 part of type AST::Parent:     ..
    Remaining unparsed: "/"
    === "." ===
    Parsed: .
     - at #0 part of type AST::Self:       .
    === ".." ===
    Parsed: ..
     - at #0 part of type AST::Parent:     ..
    === "abc/." ===
    Parsed: abc/.
     - at #0 part of type AST::Filter:     abc
     - at #1 part of type AST::Self:       .
    === "abc/.." ===
    Parsed: abc/..
     - at #0 part of type AST::Filter:     abc
     - at #1 part of type AST::Parent:     ..
    === "#(A:a,B:b)[2]" ===
    Parsed: #(A:a,B:b)[2]
     - at #0 part of type AST::Filter:     #(A:a,B:b)[2]
    === "." ===
    Parsed: .
     - at #0 part of type AST::Self:       .
    === ".." ===
    Parsed: ..
     - at #0 part of type AST::Parent:     ..
    === "?(2)%blub!()[2]" ===
    Parsed: ?(2)%(blub)!()[2]
     - at #0 part of type AST::DeepSearch: ?(2)%(blub)!()[2]
    === "?#(A:a,B:b,C:c)" ===
    Parsed: ?#(A:a,B:b,C:c)
     - at #0 part of type AST::DeepSearch: ?#(A:a,B:b,C:c)
    === "?#ABC:EFG" ===
    Parsed: ?#(ABC:EFG)
     - at #0 part of type AST::DeepSearch: ?#(ABC:EFG)
    === "?%(A,B,C)[2]" ===
    Parsed: ?%(A,B,C)[2]
     - at #0 part of type AST::DeepSearch: ?%(A,B,C)[2]
    === "?%(A,B,C)" ===
    Parsed: ?%(A,B,C)
     - at #0 part of type AST::DeepSearch: ?%(A,B,C)
    === "?%ABC[2]" ===
    Parsed: ?%(ABC)[2]
     - at #0 part of type AST::DeepSearch: ?%(ABC)[2]
    === "?%ABC" ===
    Parsed: ?%(ABC)
     - at #0 part of type AST::DeepSearch: ?%(ABC)
    === "%(A,B,C)[5]" ===
    Parsed: %(A,B,C)[5]
     - at #0 part of type AST::Filter:     %(A,B,C)[5]
    === "#(a:A,b:B)[2]" ===
    Parsed: #(a:A,b:B)[2]
     - at #0 part of type AST::Filter:     #(a:A,b:B)[2]
    === "%(A,B,C)" ===
    Parsed: %(A,B,C)
     - at #0 part of type AST::Filter:     %(A,B,C)
    === "#(a:A,b:B)" ===
    Parsed: #(a:A,b:B)
     - at #0 part of type AST::Filter:     #(a:A,b:B)
    === "_a_b4:blub" ===
    Parsed: _a_b4
     - at #0 part of type AST::Filter:     _a_b4
    Remaining unparsed: ":blub"
    === "%(A,B,C)" ===
    Parsed: %(A,B,C)
     - at #0 part of type AST::Filter:     %(A,B,C)
    === "%A" ===
    Parsed: %(A)
     - at #0 part of type AST::Filter:     %(A)
    === "abc[2]" ===
    Parsed: abc[2]
     - at #0 part of type AST::Filter:     abc[2]
    === "abc" ===
    Parsed: abc
     - at #0 part of type AST::Filter:     abc
    === "[45]" ===
    Parsed: 
    Remaining unparsed: "[45]"
    === "[2]" ===
    Parsed: 
    Remaining unparsed: "[2]"
    === "_a_b4" ===
    Parsed: _a_b4
     - at #0 part of type AST::Filter:     _a_b4
    

    Bonus

    To make all the tests that show remaining unparsed input fail instead, uncomment the >> qi::eoi expression.