Customizing the full error message for expectation failures (boost::spirit::x3)

旧时模样 提交于 2021-02-08 09:38:06

问题


The boost::spirit::x3 error handling utilities allow for the user to choose what is shown to the user when an expectation failure occurs. This, however, does not seem to be the case for the line number portion of the message, which is exactly what I'd like to modify. So instead of it printing out In line 1: etc. I would like to print some other message in it's place with the same line number info. Anyone know how I could do that, or if it is even modifiable in the first place?

EDIT:

Here's the code straight from https://www.boost.org/doc/libs/1_68_0/libs/spirit/doc/x3/html/spirit_x3/tutorials/error_handling.html:

struct error_handler
{
    template <typename Iterator, typename Exception, typename Context>
    x3::error_handler_result on_error(
        Iterator& first, Iterator const& last
      , Exception const& x, Context const& context)
    {
        auto& error_handler = x3::get<x3::error_handler_tag>(context).get();
        std::string message = "Error! Expecting: " + x.which() + " here:";
        error_handler(x.where(), message);
        return x3::error_handler_result::fail;
    }
};

In addition to the on_error function printing out the message, it prints "In line x: ", where x is the line number. I really can't have that, it does not fit in with my project in the slightest.


回答1:


Wow. First of all, I did not know all details about that example and x3::error_handler<>.

For a good break-down of how to provide error handling/diagnostic messages in X3 from basic principles, see this walk-through: Spirit X3, Is this error handling approach useful?

Traditionally (as in Qi) we would do the position tracking using an iterator adaptor:

  • Get current line in boost spirit grammar or Cross-platform way to get line number of an INI file where given option was found
  • or even the classic version of this How to pass the iterator to a function in spirit qi

At first glance it looks like the position_cache can be used separately (see eg. Boost Spirit x3 not compiling).

However, it turns out that - sadly - x3::annotate_on_success conflated the annotation task with error-handling, by assuming that position cache will always live inside the error handler. This at once means:

  • the error handler is more complicated than strictly required
  • this compounds with the fact that x3::error_handler<> is not well-suited for inheritance (due to private members and tricky to unambiguously overload operator() while keeping some overloads)
  • x3::annotate_on_success is simply not available to you unless you at least have a no-op error-handler like (Live On Coliru)

     template <typename It> struct dummy_handler_for_annotate_on_success {
        x3::position_cache<std::vector<It> > pos_cache;
        dummy_handler_for_annotate_on_success(It f, It l) : pos_cache(f,l) {}
    
        template <typename T> void tag(T& ast, It first, It last) {
            return pos_cache.annotate(ast, first, last);
        }
    };
    

    and have that present in the context under the x3::error_handler_tag for annotate_on_success to work.

  • On the positive, this does have the benefit of not requiring two separate context injections, like:

    auto const parser
        = x3::with<x3::position_cache_tag>(std::ref(pos_cache)) [
          x3::with<x3::error_handler_tag>(error_handler)
              [ parser::employees ]
          ]
        ;
    

So, here's my take on providing a custom error-handler implementation. I simplified it a bit from the built-in version¹.

One simplification is also an optimization, resting on the assumption that the iterator type is bidirectional. If not, I think you'd be better off using spirit::line_pos_iterator<> as linked above.

template <typename It> class diatnostics_handler {
    x3::position_cache<std::vector<It> > _pos_cache;
    std::ostream& _os;

  public:
    diatnostics_handler(It f, It l, std::ostream& os) : _pos_cache(f, l), _os(os) {}

    void operator()(x3::position_tagged const& ast, std::string const& error_message) const {
        auto where = _pos_cache.position_of(ast);
        operator()(where.begin(), where.end(), error_message);
    }

    void operator()(It err_first, std::string const& error_message) const {
        operator()(err_first, boost::none, error_message);
    }

    void operator()(It err_first, boost::optional<It> err_last, std::string const& error_message) const {
        auto first = _pos_cache.first(),
             last  = _pos_cache.last();

        while (err_first != last && std::isspace(*err_first))
            ++err_first;

        _os << "L:"<< line_number(err_first) << " "
            << error_message << std::endl;

        It cursor = get_line_start(first, err_first);
        print_line(cursor, last);

        auto score = [&](It& it, char fill) -> auto& {
            auto f = _os.fill();
            auto n = std::distance(cursor, it);
            cursor = it;
            return _os << std::setfill(fill) << std::setw(n) << "" << std::setfill(f);
        };
        if (err_last.has_value()) {
            score(err_first, ' ');
            score(*err_last, '~') << " <<-- Here" << std::endl;
        } else {
            score(err_first, '_') << "^_" << std::endl;
        }
    }

    template <typename AST> void tag(AST& ast, It first, It last) {
        return _pos_cache.annotate(ast, first, last);
    }

    auto const& get_position_cache() const { return _pos_cache; }

  private:
    static constexpr std::array crlf { '\r', '\n' };

    auto get_line_start(It first, It pos) const {
        return std::find_first_of( // assumed bidir iterators
                std::make_reverse_iterator(pos), std::make_reverse_iterator(first),
                crlf.begin(), crlf.end()
            ).base();
    }

    auto line_number(It i) const {
        return 1 + std::count(_pos_cache.first(), i, '\n');
    }

    void print_line(It f, It l) const {
        std::basic_string s(f, std::find_first_of(f, l, crlf.begin(), crlf.end()));
        _os << boost::locale::conv::utf_to_utf<char>(s) << std::endl;
    }
};

Which you can then demo like Live On Coliru

custom::diatnostics_handler<It> diags(iter, end, std::clog);

auto const parser
    = x3::with<x3::error_handler_tag>(std::ref(diags))
      [ parser::employees ]
    ;

std::vector<ast::employee> ast;
if (phrase_parse(iter, end, parser >> x3::eoi, x3::space, ast)) {
    std::cout << "Parsing succeeded\n";

    for (auto const& emp : ast) {
        std::cout << "got: " << emp << std::endl;

        diags(emp.who.last_name, "note: that's a nice last name");
        diags(emp.who, "warning: the whole person could be nice?");
    }
} ...

Which prints:

With custom diagnostics only:
Parsing succeeded
got: (23 (Amanda Stefanski) 1000.99)
L:1 note: that's a nice last name
{ 23, "Amanda", "Stefanski", 1000.99 },
                ~~~~~~~~~~~ <<-- Here
L:1 warning: the whole person could be nice?
{ 23, "Amanda", "Stefanski", 1000.99 },
      ~~~~~~~~~~~~~~~~~~~~~ <<-- Here
got: (35 (Angie Chilcote) 2000.99)
L:2 note: that's a nice last name
        { 35, "Angie", "Chilcote", 2000.99 }
                       ~~~~~~~~~~ <<-- Here
L:2 warning: the whole person could be nice?
        { 35, "Angie", "Chilcote", 2000.99 }
              ~~~~~~~~~~~~~~~~~~~ <<-- Here


 ----- Now with parse error:
L:3 error: expecting: person
 'Amanda', "Stefanski", 1000.99 },
_^_
Parsing failed

Simplifying Down

By breaking the false coupling between annotate_on_success and x3::error_handler_tag context, you could slim it down, a lot:

template <typename It> struct diagnostics_handler {
    It _first, _last;
    std::ostream& _os;

    void operator()(It err_first, std::string const& error_message) const {
        size_t line_no = 1;
        auto bol = _first;
        for (auto it = bol; it != err_first; ++it)
            if (*it == '\n') {
                bol = it+1;
                line_no += 1;
            }

        _os << "L:" << line_no
            << ":" << std::distance(bol, err_first)
            << " " << error_message << "\n";
    }
};

See it Live On Coliru

#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/home/x3/support/ast/position_tagged.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/include/io.hpp>

#include <iostream>
#include <iomanip>
#include <string>
namespace x3 = boost::spirit::x3;

namespace ast {
    struct name : std::string, x3::position_tagged {
        using std::string::string;
        using std::string::operator=;
    };
    struct person   : x3::position_tagged { ast::name first_name, last_name; };
    struct employee : x3::position_tagged { int age; person who; double salary; };
    using boost::fusion::operator<<;
}

BOOST_FUSION_ADAPT_STRUCT(ast::person, first_name, last_name)
BOOST_FUSION_ADAPT_STRUCT(ast::employee, age, who, salary)

namespace custom {
    struct diagnostics_handler_tag;

    template <typename It> struct diagnostics_handler {
        It _first, _last;
        std::ostream& _os;

        void operator()(It err_first, std::string const& error_message) const {
            size_t line_no = 1;
            auto bol = _first;
            for (auto it = bol; it != err_first; ++it)
                if (*it == '\n') {
                    bol = it+1;
                    line_no += 1;
                }

            _os << "L:"<< line_no
                << ":" << std::distance(bol, err_first)
                << " " << error_message << "\n";
        }
    };

} // namespace custom

namespace parser {
    namespace x3 = boost::spirit::x3;
    namespace ascii = boost::spirit::x3::ascii;

    struct error_handler {
        template <typename It, typename E, typename Ctx>
        x3::error_handler_result on_error(It&, It const&, E const& x, Ctx const& ctx) {
            auto& handler = x3::get<custom::diagnostics_handler_tag>(ctx);
            handler(x.where(), "error: expecting: " + x.which());
            return x3::error_handler_result::fail;
        }
    };

    struct annotate_position {
        template <typename T, typename Iterator, typename Context>
        inline void on_success(const Iterator &first, const Iterator &last, T &ast, const Context &context)
        {
            auto &position_cache = x3::get<annotate_position>(context).get();
            position_cache.annotate(ast, first, last);
        }
    };

    struct quoted_string_class : annotate_position {};
    struct person_class : annotate_position {};
    struct employee_class : error_handler, annotate_position {};

    x3::rule<quoted_string_class, ast::name>     const name = "name";
    x3::rule<person_class,        ast::person>   const person        = "person";
    x3::rule<employee_class,      ast::employee> const employee      = "employee";

    auto const name_def
        = x3::lexeme['"' >> +(x3::char_ - '"') >> '"']
        ;
    auto const person_def
        = name > ',' > name
        ;

    auto const employee_def
        = '{' > x3::int_ > ',' > person > ',' > x3::double_ > '}'
        ;

    BOOST_SPIRIT_DEFINE(name, person, employee)

    auto const employees = employee >> *(',' >> employee);
}

void parse(std::string const& input) {
    using It = std::string::const_iterator;

    It iter = input.begin(), end = input.end();
    x3::position_cache<std::vector<It> > pos_cache(iter, end);
    custom::diagnostics_handler<It> diags { iter, end, std::clog };

    auto const parser =
        x3::with<parser::annotate_position>(std::ref(pos_cache)) [
            x3::with<custom::diagnostics_handler_tag>(diags) [
                 parser::employees
            ]
        ];

    std::vector<ast::employee> ast;
    if (phrase_parse(iter, end, parser >> x3::eoi, x3::space, ast)) {
        std::cout << "Parsing succeeded\n";

        for (auto const& emp : ast) {
            std::cout << "got: " << emp << std::endl;

            diags(pos_cache.position_of(emp.who.last_name).begin(), "note: that's a nice last name");
            diags(pos_cache.position_of(emp.who).begin(), "warning: the whole person could be nice?");
        }
    } else {
        std::cout << "Parsing failed\n";
        ast.clear();
    }
}

static std::string const
    good_input = R"({ 23, "Amanda", "Stefanski", 1000.99 },
        { 35, "Angie", "Chilcote", 2000.99 }
    )", 
    bad_input = R"(
        { 23,
 'Amanda', "Stefanski", 1000.99 },
    )";

int main() {
    std::cout << "With custom diagnostics only:" << std::endl;
    parse(good_input);

    std::cout << "\n\n ----- Now with parse error:" << std::endl;
    parse(bad_input);
}

Prints:

With custom diagnostics only:
Parsing succeeded
got: (23 (Amanda Stefanski) 1000.99)
L:1:16 note: that's a nice last name
L:1:6 warning: the whole person could be nice?
got: (35 (Angie Chilcote) 2000.99)
L:2:23 note: that's a nice last name
L:2:14 warning: the whole person could be nice?


 ----- Now with parse error:
L:2:13 error: expecting: person
Parsing failed

¹ also fixed a bug that causes diagnostics to display wrongly on the first line(?) with x3::error_handler<> implementation



来源:https://stackoverflow.com/questions/61721421/customizing-the-full-error-message-for-expectation-failures-boostspiritx3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!