c++boostboost-spirit-x3

Boost Spirit X3: What is the difference between `_val` and `_attr`?


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 and x3::_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?


Solution

  • _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_ or double_, return the int or double value converted from the matched input sequence. Other primitive parser components have other intuitive attribute types, such as for instance int_ which has int, or ascii::char_ which has char

    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:

    Live On Coliru

    #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:

    Live On Coliru

    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)