4
\$\begingroup\$

While exploring Boost Spirit X3 (documentation), I noticed that I couldn't find a parser directive that composed parsers. I dug through the source code to see how the existing directives were made, and built one.

The idea is to have a parser A with an attribute that can be coerced to std::string, and a parser B; compose(B)[A] would run B on the output of A.

I'm a bit worried that I re-invented the wheel, or that what I did goes against the spirit of Spirit X3. I'm also worried about the efficiency or usability of it - I use x3::parse internally, which might also make error handling hard.

compose.hpp

#pragma once

#include <boost/spirit/home/x3.hpp>

namespace x3_extra {
    namespace x3 = boost::spirit::x3;

    template <typename Primary, typename Secondary>
    class compose_directive
        : public x3::unary_parser<Primary, compose_directive<Primary, Secondary>>
    {
        Secondary secondary_;

    public:
        using base_type = x3::unary_parser<Primary, compose_directive<Primary, Secondary>>;
        static constexpr bool has_attribute = true;
        using attribute_type = typename x3::traits::attribute_of<Secondary, void>::type;

        template <typename TPrimary, typename TSecondary>
        compose_directive(TPrimary&& primary, TSecondary&& secondary)
            : base_type{std::forward<TPrimary>(primary)}
            , secondary_{std::forward<TSecondary>(secondary)}
        {}

        template <typename Iterator, typename Context, typename RContext, typename Attribute>
        bool parse(Iterator& first, Iterator const& last, Context const& context,
                   RContext& rcontext, Attribute& attr) const
        {
            std::string primaryResult;
            bool isPrimarySuccess
                = this->subject.parse(first, last, context, rcontext, primaryResult);
            if (!isPrimarySuccess) return false;

            auto secondaryFirst = primaryResult.begin();
            auto secondaryLast = primaryResult.end();

            attribute_type result;
            auto const isSecondarySuccess
                = x3::parse(secondaryFirst, secondaryLast, secondary_, result);
            if (!isSecondarySuccess) return false;
            if (secondaryFirst != secondaryLast) return false;

            x3::traits::move_to(result, attr);
            return true;
        }
    };

    template <typename Secondary>
    struct compose_gen
    {
        Secondary secondary;

        template <typename TPrimary>
        auto operator[](TPrimary&& primary) const&
        {
            return compose_directive<std::decay_t<TPrimary>, Secondary>{
                std::forward<TPrimary>(primary), secondary};
        }

        template <typename TPrimary>
        auto operator[](TPrimary&& primary) &&
        {
            return compose_directive<std::decay_t<TPrimary>, Secondary>{
                std::forward<TPrimary>(primary), std::move(secondary)};
        }
    };

    template <typename Secondary>
    auto compose(Secondary secondary)
    {
        return compose_gen<Secondary>{std::move(secondary)};
    }
}

Example usage:

// parse ints with underscores ('_') in the middle
auto recognizeInt = x3::char_("0-9") >> *(*x3::lit('_') >> +x3::char_("0-9"));
// convert said strings into ints
auto parseInt = x3_extra::compose(x3::int_)[recognizeInt];
\$\endgroup\$
6
  • \$\begingroup\$ I believe there was a cppcon talk, which explained things very well. From what I remember, it should be just >> again. \$\endgroup\$ Aug 21, 2017 at 22:26
  • \$\begingroup\$ @Incomputable yes michael caisse gave a cppcon talk (the only one I could find on X3, but there's a couple BoostCon ones). >> doesn't "compose" parsers (like function composition), it "concatenates" parsers \$\endgroup\$
    – Justin
    Aug 21, 2017 at 22:28
  • \$\begingroup\$ Then I probably don't know what is the difference. The former could be appending, whereas the latter could be reordering? That is the only difference I can think of, and I never encountered the latter. \$\endgroup\$ Aug 21, 2017 at 22:31
  • \$\begingroup\$ @Incomputable composition: f (g (str)). Concatenation: f (str) then g (str-remaining) \$\endgroup\$
    – Justin
    Aug 21, 2017 at 22:35
  • \$\begingroup\$ It looks like the as<T>[]()[] directive should do what I want, but it doesn't seem to exist \$\endgroup\$
    – Justin
    Aug 21, 2017 at 22:58

1 Answer 1

1
\$\begingroup\$

@Justin, I'd suggest filter_input as a more descriptive name since that's, in essence, what it does. It changes the input to the Secondary parser using the std::string attribute calculated from the Primary parser.

Compose is too general a name since each operator in x3/operator/*.hpp is, in effect, a composer of their sub-parsers.

@Justin, I'm mystified by your mention of as<T>[]()[] directive. Is it somehow related to as<T>[] mentioned here? If so, what are the arg to the trailing ()[]?

In short, compose modifies the input; whereas as<T>[p] modifies the attribute.

BTW, first need to be restored to it's original value if either Primary or Secondary fails.

\$\endgroup\$
2
  • \$\begingroup\$ Hopefully my edited post satisfies the requirement to review the code. \$\endgroup\$ Nov 19, 2023 at 21:26
  • \$\begingroup\$ Sorry for the delay, yes I think you have thanks \$\endgroup\$
    – Peilonrayz
    Dec 2, 2023 at 17:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.