I am looking into Boost Spirit X3, but am struggling to fully grasp the difference between x3::_val
and x3::_attr
as used in semantic actions to extract attributes from the passed context. The official docs state
_val: A reference to the attribute of the innermost rule that directly or indirectly invokes the parser p
_attr: A reference to the attribute of the parser p
which doesn't really help me. I researched some and stumbled upon https://stackoverflow.com/a/61703199/3907364, which states
x3::_val(ctx) is like qi::_val
x3::_attr(ctx) is like qi::_0 (or qi::_1 for simple parsers)
but unfortunately, I was unsuccessful at figuring out what qi::_val
and qi::_0
/qi::_1
are - or more precisely what their difference is.
Finally, I also found https://stackoverflow.com/a/53971888/3907364, where it is stated that
What they [
x3::_val
andx3::_attr
] will be in a particular situation depends on make_attribute/transform_attribute traits. By default they will reference to the same value until you have nested rules with different attribute types
which seems to conform with experiments that I have conducted thus far - except I have not yet managed for _attr
and _val
to yield different values.
Even though I was unable to understand their difference yet, it seems rather relevant as all examples that I have seen, that use semantic actions to e.g. compute the result of a given calculation (see e.g. here) always seem to use _attr
as a more global state, whereas _val
seems to be the immediate state of the thing that has just been parsed. E.g.
[](auto& ctx) { _val(ctx) = _val(ctx) * _attr(ctx); }
Yet with all of this, I am still not quite able to point my finger to the exact difference in semantics between the two properties. Could someone perhaps try to rephrase Boost's docs and give an example of where the difference is actually important/visible?
_val: A reference to the attribute of the innermost rule that directly or indirectly invokes the parser p
_attr: A reference to the attribute of the parser p
Since this didn't help you, let's illustrate.
I like to call _attr
the (accessor for) the synthesized attribute:
Parsers and generators in Spirit are fully attributed. Spirit.Qi parsers always expose an attribute specific to their type. This is called synthesized attribute as it is returned from a successful match representing the matched input sequence.
For instance, numeric parsers, such as
int_
ordouble_
, return theint
ordouble
value converted from the matched input sequence. Other primitive parser components have other intuitive attribute types, such as for instanceint_
which hasint
, orascii::char_
which haschar
On the other hand, there is the attribute bound to the rule when invoking a parser, the _val(ctx)
. This depends on what context you use it in.
Here's a demonstration program illustrating the differences:
#include <boost/fusion/adapted.hpp>
#include <boost/fusion/include/as_vector.hpp>
#include <boost/fusion/include/io.hpp>
#include <boost/spirit/home/x3.hpp>
#include <iostream>
using boost::core::demangle;
namespace x3 = boost::spirit::x3;
namespace Parser {
using boost::fusion::at_c;
auto action = [](auto& ctx) {
using Synthesized = decltype(_attr(ctx));
using Bound = decltype(_val(ctx));
std::cout << "synthesized: " << demangle(typeid(Synthesized).name()) << "\n";
std::cout << "bound: " << demangle(typeid(Bound).name()) << "\n";
if constexpr (!std::is_same_v<x3::unused_type, std::decay_t<Bound>>) {
at_c<0>(_val(ctx)) = at_c<0>(_attr(ctx));
at_c<1>(_val(ctx)) = at_c<1>(_attr(ctx));
}
};
auto expr = (x3::int_ >> x3::int_)[action];
template <typename T> auto rule = x3::rule<struct Tag, T>{"rule"} = expr;
} // namespace Parser
template <typename T = x3::unused_type> void test() {
static constexpr std::string_view input = "123 234";
if constexpr (std::is_same_v<x3::unused_type, T>) {
phrase_parse(begin(input), end(input), Parser::rule<T>, x3::space);
std::cout << " -> (no attribute)\n\n";
} else {
T attr;
phrase_parse(begin(input), end(input), Parser::rule<T>, x3::space, attr);
std::cout << " -> " << boost::fusion::as_vector(attr) << "\n\n";
}
}
int main() {
test();
test<boost::fusion::deque<int, int>>();
test<std::pair<int, int>>();
test<std::tuple<int, int>>();
}
Printing:
synthesized: boost::fusion::deque<int, int>
bound: boost::spirit::x3::unused_type
-> (no attribute)
synthesized: boost::fusion::deque<int, int>
bound: boost::fusion::deque<int, int>
-> (123 234)
synthesized: boost::fusion::deque<int, int>
bound: std::pair<int, int>
-> (123 234)
synthesized: boost::fusion::deque<int, int>
bound: std::tuple<int, int>
-> (123 234)
Note that the action is completely redundant here, just used to add output:
template <typename T = x3::unused_type> void test() {
static constexpr std::string_view input = "123 234";
T attr;
phrase_parse(begin(input), end(input), x3::int_ >> x3::int_, x3::space, attr);
if constexpr (std::is_same_v<x3::unused_type, T>)
std::cout << " -> (no attribute)\n";
else
std::cout << " -> " << boost::fusion::as_vector(attr) << "\n";
}
int main() {
test();
test<boost::fusion::deque<int, int>>();
test<std::pair<int, int>>();
test<std::tuple<int, int>>();
}
Still printing
-> (no attribute)
-> (123 234)
-> (123 234)
-> (123 234)