File: minimal.qbk

package info (click to toggle)
boost1.74 1.74.0-9
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 464,084 kB
  • sloc: cpp: 3,338,324; xml: 131,293; python: 33,088; ansic: 14,336; asm: 4,034; sh: 3,351; makefile: 1,193; perl: 1,036; yacc: 478; php: 212; ruby: 102; lisp: 24; sql: 13; csh: 6
file content (247 lines) | stat: -rw-r--r-- 9,591 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
[/==============================================================================
    Copyright (C) 2001-2018 Joel de Guzman

    Distributed under the Boost Software License, Version 1.0. (See accompanying
    file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

    I would like to thank Rainbowverse, llc (https://primeorbial.com/)
    for sponsoring this work and donating it to the community.
===============================================================================/]

[section:minimal X3 Program Structure]

As a prerequisite in understanding this tutorial, please review the previous
[tutorial_employee 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 [tutorial_employee 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.

[heading Structure]

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

[table
    [[`File`            ]                                               [Description            ]]
    [[[@../../../example/x3/minimal/ast.hpp ast.hpp]]                   [The AST                ]]
    [[[@../../../example/x3/minimal/ast_adapted.hpp ast_adapted.hpp]]   [Fusion adapters        ]]
    [[[@../../../example/x3/minimal/config.hpp config.hpp]]             [Configuration          ]]
    [[[@../../../example/x3/minimal/employee.hpp employee.hpp]]         [Main parser API        ]]
    [[[@../../../example/x3/minimal/employee_def.hpp employee_def.hpp]] [Parser definitions     ]]
    [[[@../../../example/x3/minimal/employee.cpp employee.cpp]]         [Parser instantiation   ]]
    [[[@../../../example/x3/minimal/main.cpp main.cpp]]                 [Main program           ]]
]

The contents of the files should already be familiar. It's essentially the
same [tutorial_employee 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.

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

[heading 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
   )

[heading Main parser API]

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

[#__tutorial_spirit_declare__]
[heading BOOST_SPIRIT_DECLARE]

Remember [link __tutorial_spirit_define__ `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>
    bool parse_rule(
        decltype(my_rule)
      , Iterator& first, Iterator const& last
      , Context const& context, decltype(my_rule)::attribute_type& attr);

If you went back and reviewed [link __tutorial_spirit_define__
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 `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.

[heading 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 [link __tutorial_spirit_define__
`BOOST_SPIRIT_DEFINE`] just like we did in the [tutorial_employee 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.

[#tutorial_configuration]
[heading Configuration]

Here, we declare some types for instatntaiting our X3 parser with. Rememeber
that Spirit parsers can work with any __fwditer__. 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`.

[heading 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.

[heading BOOST_SPIRIT_INSTANTIATE]

Go back and review [link __tutorial_spirit_define__ `BOOST_SPIRIT_DEFINE`]
and [link __tutorial_spirit_declare__ `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 [@../../../example/x3/minimal/employee.cpp
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 ([@../../../example/x3/minimal/employee.hpp 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 rule_
      , iterator_type& first, iterator_type const& last
      , context_type const& context, employee_type::attribute_type& attr);

[heading Main Program]

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

    #include "ast.hpp"
    #include "ast_adapted.hpp"
    #include "employee.hpp"

# `ast.hpp` for the AST declaration
# `ast_adapted.hpp` if you need to traverse the AST using fusion
# `employee.hpp` the main parser API

[endsect]