c++boost-spirit

having an optional at front in a expected sequence list disallows an empty string


i have a comma separated list of id pairs, empty list is also valid

a b, c d

my rule is a expected sequence of id > id

that works

but when i try to add an optional integer value in front like -qi::int_ > id > id

a b, 2 c d

the empty list is not allowed anymore and i get an expectation exception asking for an id

Example: Live on Coliru

// #define BOOST_SPIRIT_DEBUG
#include <boost/phoenix.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iomanip>
#include <optional>
namespace qi = boost::spirit::qi;

namespace Ast
{
    struct Identifier : std::string
    {
        using std::string::string;
        using std::string::operator=;
    };
    struct Test
    {
        std::optional<int> a;
        Identifier b, c;
    };

} // namespace Ast

BOOST_FUSION_ADAPT_STRUCT( Ast::Test, a, b, c )

using It = std::string_view::const_iterator;

template <typename Attr, typename Skipper>
void run_tests( qi::rule<It, Attr(), Skipper> const& rule_, std::vector<std::string> const& tests )
{
    using boost::core::demangle;
    std::cout << "======[ " << demangle( typeid( Attr ).name() ) << " ]======" << std::endl;
    for( std::string_view test : tests )
    {
        It f = test.begin(), l = test.end();
        try
        {
            std::cout << "Parsing: " << quoted( test ) << " -> ";
            Attr v;
            bool ok;

            if constexpr( std::is_same_v<Skipper, qi::unused_type> )
                ok = parse( f, l, qi::eps > rule_ > qi::eoi, v );
            else
                ok = phrase_parse( f, l, qi::eps > rule_ > qi::eoi, Skipper{}, v );

            std::cout << ( ok ? "OK" : "FAIL" )                          //
                      << " Remaining: " << quoted( std::string( f, l ) ) //
                      << "\n";
        }
        catch( qi::expectation_failure<It> const& ef )
        {
            auto p = ef.first - test.begin();
            auto bol = test.find_last_of( "\r\n", p ) + 1;
            auto line = std::count( f, f + bol, '\n' ) + 1;
            auto eol = test.find_first_of( "\r\n", p );

            std::cout << "EXPECTED " << ef.what_ << " in line:" << line << " col:" << ( p - bol ) << "\n"
                      << "    " << test.substr( bol, eol - bol ) << "\n"
                      << "    " << std::setw( p - bol ) << "" << "^--- here\n";
        }
    }
}

template <typename Attr>
using Rule = qi::rule<It, Attr(), qi::space_type>;

int main()
{
    qi::rule<It, Ast::Identifier()> id = qi::char_( "a-zA-Z_" ) >> *qi::char_( "a-zA-Z0-9_" );
    Rule<Ast::Test> test = -qi::int_ > id > id;
    Rule<std::vector<Ast::Test>> list = -( test % ',' );
    BOOST_SPIRIT_DEBUG_NODES( (id)(test)( list ) )

    run_tests( list, { "a b, c d", "" } );
}

Output:

Parsing: "a b, c d" -> OK Remaining: ""
Parsing: "" -> EXPECTED <id> in line:1 col:0

i don't see why adding the optional int breaks the rule of -( test % ',' ) that it can be also empty


Solution

  • You have to tell -qi::int_ to NOT match if nothing follows:

    Rule<Ast::Test>  test = (!qi::eoi >> -qi::int_) > id > id;
    

    See Live

    Printing

    Parsing: "a b, 2 c d" -> OK Remaining: ""
    Parsing: "" -> OK Remaining: ""
    

    Alternatively, be less overstrict with expectations. After all, your expectation WASN'T "if no integer was matched, require an id":

    Rule<Ast::Test> test = -qi::int_ >> id >> id;
    

    Same output, Live