Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Error Handling

This tutorial wouldn't be complete without touching on error handling. As a prerequisite in understanding this tutorial, please review the previous employee and annotations examples. This example builds on top of these previous examples.

The full cpp file for this example can be found here: error_handling.cpp

Please review the previous annotations example. The information there will be very helpful in understanding error handling.

The AST

Our AST is exactly the same as what we had before in the annotations:

namespace client { namespace ast
{
    struct person : x3::position_tagged
    {
        person(
            std::string const& first_name = ""
          , std::string const& last_name = ""
        )
        : first_name(first_name)
        , last_name(last_name)
        {}

        std::string first_name, last_name;
    };

    struct employee : x3::position_tagged
    {
        int age;
        person who;
        double salary;
    };
}}

We have two structs, the person and the employee. Each inherits from x3::position_tagged which provides positional information that we can use to tell the AST's position in the input stream anytime. We will need these information for error handling and reporting.

Like before, we need to tell Boost.Fusion about our structs to make them first-class fusion citizens that the grammar can utilize:

BOOST_FUSION_ADAPT_STRUCT(client::ast::person,
    first_name, last_name
)

BOOST_FUSION_ADAPT_STRUCT(client::ast::employee,
    age, who, salary
)
Expectations

There are occasions in which it is expected that the input must match a particular parser or the input is invalid. Such cases generally arise after matching a portion of a grammar, such that the context is fully known. In such a situation, failure to match should result in an exception. For example, when parsing an e-mail address, a name, an "@" and a domain name must be matched or the address is invalid.

Two X3 mechanisms facilitate parser expectations:

  1. The expectation operator (Expectation operator)
  2. The expect directive (expect[p])

The expectation operator (Expectation operator) requires that the following parser (b) match the input or an expectation_failure is emitted. Using a client supplied on_error handler, the exception can be serviced by calling the handler with the source iterators and context at which the parsing failed can be reported.

By contrast, the sequence operator (Sequence) does not require that the following parser match the input, which allows for backtracking or simply returning false from the parse function with no exceptions.

The expect directive (expect[p]) requires that the argument parser matches the input or an exception is emitted. Using on_error(), that exception can be handled by calling a handler with the context at which the parsing failed can be reported.

on_error

on_error is the counterpart of on_success, as discussed in the annotations example. While on_success handlers are callback hooks to client code that are executed by the parser after a successful parse, on_error handlers are callback hooks to client code that are executed by the parser when an expectation_failure is thrown via the expect operator or directive. on_error handlers have access to the iterators, the context and the exception that was thrown.

Error Handling

Before we proceed, let me introduce a helper class, the x3::error_handler. It is utility class that provides Clang style error reporting which gives you nice reports such as the following:

In line 16:
Error! Expecting: person here:
    'I am not a person!'    <--- this should be a person
____^_

We'll see later that this error message is exactly what this example emits.

Here's our on_error handler:

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;
    }
};

x3::error_handler_tag is a special tag we will use to get a reference to the actual x3::error_handler that we will inject at very start, when we call parse. We get the x3::error_handler here:

auto& error_handler = x3::get<error_handler_tag>(context).get();

The x3::error_handler handles all the nitty gritty details such as determining the line number and actual column position, and formatting the error message printed. All we have to do is provide the actual error string which we extract from the expectation_failure exception:

std::string message = "Error! Expecting: " + x.which() + " here:";

Then, we return x3::error_handler_result::fail to tell X3 that we want to fail the parse when such an event is caught. You can return one of:

Action

Description

fail

Quit and fail. Return a no_match.

retry

Attempt error recovery, possibly moving the iterator position.

accept

Force success, moving the iterator position appropriately.

rethrow

Rethrows the error.

The Parser

Now we'll rewrite employee parser with error handling in mind. Like the annotations example, inputs will be of the form:

{ age, "forename", "surname", salary }

Here we go:

namespace parser
{
    using x3::int_;
    using x3::double_;
    using x3::lexeme;
    using ascii::char_;

    struct quoted_string_class;
    struct person_class;
    struct employee_class;

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

    auto const quoted_string_def = lexeme['"' >> +(char_ - '"') >> '"'];
    auto const person_def = quoted_string > ',' > quoted_string;

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

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

    BOOST_SPIRIT_DEFINE(quoted_string, person, employee);

    struct quoted_string_class {};
    struct person_class : x3::annotate_on_success {};
    struct employee_class : error_handler, x3::annotate_on_success {};
}

Go back and review the annotated employee parser. What has changed? It is almost identical, except:

Where appropriate, we're using the expectation operator (Expectation operator) in place of the sequence operator (Sequence):

auto const person_def = quoted_string > ',' > quoted_string;

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

You will have some "deterministic points" in the grammar. Those are the places where backtracking cannot occur. For our example above, when you get a '{', you definitely must see an int_ next. After that, you definitely must have a ',' next and then a person and so on until the final '}'. Otherwise, there is no point in proceeding and trying other branches, regardless where they are. The input is definitely erroneous. When this happens, an expectation_failure exception is thrown. Somewhere outward, the error handler will catch the exception. In our case, it is caught in our on_error handler.

Notice too that we subclass the employee_class from our error_handler. By doing so, we tell X3 that we want to call our error_handler whenever an exception is thrown somewhere inside the employee rule and whatever else it calls (i.e. the person and quoted_string rules).

Let's Parse

Now we have the complete parse mechanism with error handling:

void parse(std::string const& input)
{
    using boost::spirit::x3::ascii::space;
    typedef std::string::const_iterator iterator_type;

    std::vector<client::ast::employee> ast;
    iterator_type iter = input.begin();
    iterator_type const end = input.end();

    using boost::spirit::x3::with;
    using boost::spirit::x3::error_handler_tag;
    using error_handler_type = boost::spirit::x3::error_handler<iterator_type>;

    // Our error handler
    error_handler_type error_handler(iter, end, std::cerr);

    // Our parser
    using client::parser::employees;
    auto const parser =
        // we pass our error handler to the parser so we can access
        // it later in our on_error and on_sucess handlers
        with<error_handler_tag>(std::ref(error_handler))
        [
            employees
        ];

    bool r = phrase_parse(iter, end, parser, space, ast);

    // ... Some final reports here
}

Prior to calling phrase_parse, we first create an AST where parsed data will be stored:

std::vector<client::ast::employee> ast;

We also create the actual error handler, sending message to std::cerr:

error_handler_type error_handler(iter, end, std::cerr);

Then, we inject a reference to error_handler, using the with directive similar to what we did in the annotations example:

auto const parser =
    // we pass our error handler to the parser so we can access
    // it later in our on_error and on_sucess handlers
    with<error_handler_tag>(std::ref(error_handler))
    [
        employees
    ];

Now, if we give the parser an erroneous input:

std::string bad_input = R"(
{
    23,
    "Amanda",
    "Stefanski",
    1000.99
},
{
    35,
    "Angie",
    "Chilcote",
    2000.99
},
{
    43,
    'I am not a person!'    <--- this should be a person
    3000.99
},
{
    22,
    "Dorene",
    "Dole",
    2500.99
},
{
    38,
    "Rossana",
    "Rafferty",
    5000.99
}
)";

The parser will complain as expected:

-------------------------
Now we have some errors
In line 16:
Error! Expecting: person here:
    'I am not a person!'    <--- this should be a person
____^_
-------------------------
Parsing failed
-------------------------

PrevUpHomeNext