Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

X3 Program Structure

As a prerequisite in understanding this tutorial, please review the previous employee example. This example builds on top of that example.

So far, to keep things simple, all of the tutorial programs are self contained in one cpp file. In reality, you will want to separate various logical modules of the parser into separate cpp and header files, decoupling the interface from the implememtation.

There are many ways to structure an X3 parser, but the "minimal" example in this tutorial shows the preferred way. This example basically reuses the same parser as the employee example for the sake of familiarity, but structured to allow separate compilation of the actual parser in its own definition file and cpp file. The cpp files, including main see only the header files --the interfaces. This is a good example on how X3 parsers are structured in a C++ application.

Structure

The program is structured in a directory with the following header and cpp files:

File

Description

ast.hpp

The AST

ast_adapted.hpp

Fusion adapters

config.hpp

Configuration

employee.hpp

Main parser API

employee_def.hpp

Parser definitions

employee.cpp

Parser instantiation

main.cpp

Main program

The contents of the files should already be familiar. It's essentially the same employee example. So I will skip the details on how the parser works and focus only on the features needed for refactoring the program into a modular structure suitable for real-world deployment.

AST

We place the AST declaration here:

namespace client { namespace ast
{
    struct employee
    {
        int age;
        std::string forename;
        std::string surname;
        double salary;
    };

    using boost::fusion::operator<<;
}}
Fusion adapters

Here, we adapt the AST for Fusion, making it a first-class fusion citizen:

BOOST_FUSION_ADAPT_STRUCT(client::ast::employee,
   age, forename, surname, salary
)
Main parser API

This is the main header file that all other cpp files need to include.

BOOST_SPIRIT_DECLARE

Remember BOOST_SPIRIT_DEFINE? If not, then you probably want to go back and review that section to get a better understanding of what's happening.

Here in the header file, instead of BOOST_SPIRIT_DEFINE, we use BOOST_SPIRIT_DECLARE for the top rule. Behind the scenes, what's actually happening is that we are declaring a parse_rule function in the client namespace. For example, given a rule named my_rule, BOOST_SPIRIT_DECLARE(my_rule) expands to this code:

template <typename Iterator, typename Context, typename Attribute>
bool parse_rule(
    decltype(my_rule)
  , Iterator& first, Iterator const& last
  , Context const& context, Attribute& attr);

If you went back and reviewed BOOST_SPIRIT_DEFINE, you'll see why it is exactly what we need to use for header files. BOOST_SPIRIT_DECLARE generates function declarations that are meant to be placed in hpp (header) files while BOOST_SPIRIT_DEFINE generates function definitions that are meant to be placed in cpp files.

[Note] Note

BOOST_SPIRIT_DECLARE is variadic and may be used for one or more rules. Example: BOOST_SPIRIT_DECLARE(r1, r2, r3);

In this example, the top rule is employee. We declare employee in this header file:

namespace client
{
    namespace parser
    {
        namespace x3 = boost::spirit::x3;
        using employee_type = x3::rule<class employee, ast::employee>;
        BOOST_SPIRIT_DECLARE(employee_type);
    }

    parser::employee_type employee();
}

We also provide a function that returns an employee object. This is the parser that we will use anywhere it is needed. X3 parser objects are very lightweight. They are basically simple tags with no data other than the name of the rule (e.g. "employee"). Notice that we are passing this by value.

Parser Definitions

Here is where we place the actual rules that make up our grammar:

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

    using x3::int_;
    using x3::lit;
    using x3::double_;
    using x3::lexeme;
    using ascii::char_;

    x3::rule<class employee, ast::employee> const employee = "employee";

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

    auto const employee_def =
        lit("employee")
        >> '{'
        >>  int_ >> ','
        >>  quoted_string >> ','
        >>  quoted_string >> ','
        >>  double_
        >>  '}'
        ;

    BOOST_SPIRIT_DEFINE(employee);
}

parser::employee_type employee()
{
    return parser::employee;
}

In the parser definition, we use BOOST_SPIRIT_DEFINE just like we did in the employee example.

While this is another header file, it is not meant to be included by the client. Its purpose is to be included by an instantiations cpp file (see below). We place this in an .hpp file for flexibility, so we have the freedom to instantiate the parser with different iterator types.

Configuration

Here, we declare some types for instatntaiting our X3 parser with. Rememeber that Spirit parsers can work with any ForwardIterator. We'll also need to provide the initial context type. This is the context that X3 will use to initiate a parse. For calling phrase_parse, you will need the phrase_parse_context like we do below, passing in the skipper type.

using iterator_type = std::string::const_iterator;
using context_type = x3::phrase_parse_context<x3::ascii::space_type>::type;

For plain parse, we simply use x3::unused_type.

Parser Instantiation

Now we instantiate our parser here, for our specific configuration:

namespace client { namespace parser
{
    BOOST_SPIRIT_INSTANTIATE(employee_type, iterator_type, context_type);
}}

For that, we use BOOST_SPIRIT_INSTANTIATE, passing in the parser type, the iterator type, and the context type.

BOOST_SPIRIT_INSTANTIATE

Go back and review BOOST_SPIRIT_DEFINE and BOOST_SPIRIT_DECLARE to get a better grasp of what's happening with BOOST_SPIRIT_INSTANTIATE and why it is needed.

So what the heck is BOOST_SPIRIT_INSTANTIATE? What we want is to isolate the instantiation of our parsers (rules and all that), into separate translation units (or cpp files, if you will). In this example, we want to place our x3 employee stuff in employee.cpp. That way, we have separate compilation. Every time we update our employee parser source code, we only have to build the employee.cpp file. All the rest will not be affected. By compiling only once in one translation unit, we save on build times and avoid code bloat. There is no code duplication, which can happen otherwise if you simply include the employee parser (employee.hpp) everywhere.

But how do you do that. Remember that our parser definitions are also placed in its own header file for flexibility, so we have the freedom to instantiate the parser with different iterator types.

What we need to do is explicitly instantiate the parse_rule function we declared and defined via BOOST_SPIRIT_DECLARE and BOOST_SPIRIT_DEFINE respectively, using BOOST_SPIRIT_INSTANTIATE. For our particular example, BOOST_SPIRIT_INSTANTIATE expands to this code:

template bool parse_rule<iterator_type, context_type, employee_type::attribute_type>(
    employee_type rule_
  , iterator_type& first, iterator_type const& last
  , context_type const& context, employee_type::attribute_type& attr);
Main Program

Finally, we have our main program. The code is the same as single cpp file employee example, but here, we simply include three header files:

#include "ast.hpp"
#include "ast_adapted.hpp"
#include "employee.hpp"
  1. ast.hpp for the AST declaration
  2. ast_adapted.hpp if you need to traverse the AST using fusion
  3. employee.hpp the main parser API

PrevUpHomeNext