Search code examples
c++cssc++17boost-spiritboost-spirit-x3

Passing each element of a parsed sequence to a function that returns a rule's attribute type


I want to parse CSS color functions (for simplicity, all of the arguments are numbers between 0 and 255)

rgb(r,g,b)
rgba(r,g,b,a)
hsl(h,s,l)
hsla(h,s,l,a)

into

struct color
{
    color(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) : red{r}, green{g}, blue{b}, alpha{a} {}
    static color hsl(std::uint8_t h, std::uint8_t s, std::uint8_t l, std::uint8_t a) { ... }
    std::uint8_t red;
    std::uint8_t green;
    std::uint8_t blue;
    std::uint8_t alpha;
}

I have a working hsl function implementation that converts the h, s, and l into rgb values.

I also have rules to handle the first two functions:

constexpr auto uint8 = uint_parser<std::uint8_t>{};
const auto color_rgb = rule<struct rgb, color>{"rgb"}
                     = lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')' >> attr(255);
const auto color_rgba = rule<struct rgba, color>{"rgba"}
                      = lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';

This works because I have used

BOOST_FUSION_ADAPT_STRUCT(color,
                          red, green, blue, alpha)

The problem lies in the hsl functions. I cannot do a second BOOST_FUSION_ADAPT_STRUCT, so I thought of using semantic actions on the respective sequences of values where the semantic action would simply construct the color from the sequence. Something like this:

const auto color_hsl = rule<struct hsl, color, true>{"hsl"}
                   = (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')' >> attr(255))[color::hsl];

This doesn't work, or I wouldn't be asking this question. Neither does Boost.Spirit.X3 have something in the sense of

[ qi::_val = phx::construct<color>(...), qi::_1, qi::_2, qi::_3, qi::_4) ];

It seems I want to do manually what BOOST_FUSION_ADAPT_STRUCT does, but in a semantic action. Is this possible and how should I handle this? I know the attribute of the sequence should be an "fusion-vector" tuple-like entity with the four parsed values. I want to extract them and stuff them into color::hsl to generate the rule's attribute.


Solution

  • Many hints apply here.

    1. I cannot do a second BOOST_FUSION_ADAPT_STRUCT

      Sure you can, see BOOST_FUSION_ADAPT_STRUCT_NAMED

    2. In Qi this general form seem to apply:

      [ qi::_val = phxfunction(qi::_0) ]
      

      You could further simplify that by making your own actor type so you could just supply your "factory action": [ factory(&Foo::makeBar) ].

      If you throw in an implementation of fusion::apply¹ you can avoid ever dealing with the Fusion sequence manuallt

    3. However, you might want to learn about this - very well hidden - attribute compatibility mode for semantic actions: BOOST_SPIRIT_ACTIONS_ALLOW_ATTR_COMPAT. Buried in that change log:

      Semantic actions now support attribute compatibility. This is a breaking change but #define BOOST_SPIRIT_ACTIONS_ALLOW_ATTR_COMPAT must be defined in order for the new behavior to kick in. By default, the old behavior is still in place.

      You might get the behaviour you desire with much less tweaking.

    4. X3 is really malleable. We can have that factory helper described above in as little as:

      auto factory = [](auto f) {
          return [f](auto& ctx) {
              x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
          };
      };
      

      I'll throw in a quick draft of my_apply (for the boost::fusion::apply described earlier):

      namespace detail {
          template <class F, class Sequence, std::size_t... I>
              constexpr decltype(auto) apply_impl(F&& f, Sequence&& t, std::index_sequence<I...>)
              {
                  return std::invoke(std::forward<F>(f), boost::fusion::at_c<I>(std::forward<Sequence>(t))...);
              }
      }
      
      template <class F, class Sequence>
          constexpr decltype(auto) my_apply(F&& f, Sequence&& t)
          {
              return detail::apply_impl(
                      std::forward<F>(f), std::forward<Sequence>(t),
                      std::make_index_sequence<typename boost::fusion::result_of::size<std::remove_reference_t<Sequence> >::type{}>{});
          }
      

    Now we can have the parser:

    namespace parser {
        using namespace x3;
    
        constexpr auto uint8  = uint_parser<std::uint8_t>{};
    
        auto factory = [](auto f) {
            return [f](auto& ctx) {
                x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
            };
        };
    
        const auto color_rgb  = rule<struct rgb, ast::color>{"rgb"}
                              = lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> x3::attr(255u) >> ')';
        const auto color_rgba = rule<struct rgba, ast::color>{"rgba"}
                              = lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';
        const auto color_hsl  = rule<struct hsl, ast::color>{"hsl"}
                              = (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> attr(255u) >> ')') [factory(ast::color::hsl)];
    
        const auto color = skip(space) [ color_rgba | color_rgb | color_hsl ];
    } 
    

    And test it with:

    int main() {
        for (std::string const input : {
                "rgb(1,2,3)",
                "rgba(4,5,6,7)",
                "hsl(8,9,10)" }) 
        {
            std::cout << " ----- Parsng " << std::quoted(input) << " --------\n";
            auto begin = input.begin(), end = input.end();
    
            ast::color result;
            bool success = parse(begin, end, parser::color, result);
    
            if (success) {
                std::cout << "parsed: ";
                std::cout << result << "\n";
            } else {
                std::cout << "failed\n";
            }
    
            if (begin != end) {
                std::cout << "Remaining unparsed: " << std::quoted(std::string(begin, end)) << std::endl;
            }
        }
    }
    

    Prints:

    Live On Coliru

     ----- Parsng "rgb(1,2,3)" --------
    parsed: rgba(1,2,3,255)
     ----- Parsng "rgba(4,5,6,7)" --------
    parsed: rgba(4,5,6,7)
     ----- Parsng "hsl(8,9,10)" --------
    TODO: implement static ast::color ast::color::hsl(uint8_t, uint8_t, uint8_t, uint8_t)(8,9,10,255)
    parsed: rgba(8,9,10,255)
    

    Full Listing

    Live On Coliru

    #include <iostream>
    #include <iomanip>
    #include <boost/spirit/home/x3.hpp>
    #include <boost/fusion/adapted/struct.hpp>
    #include <boost/fusion/include/size.hpp>
    
    namespace x3 = boost::spirit::x3;
    
    
    namespace ast {
        using std::uint8_t;
    
        struct color {
            uint8_t red, green, blue, alpha;
    
            color(uint8_t r=0, uint8_t g=0, uint8_t b=0, uint8_t a=255) : red{r}, green{g}, blue{b}, alpha{a} {}
    
            static color hsl(uint8_t h, uint8_t s, uint8_t l, uint8_t a) { 
                std::cerr << "TODO: implement " << __PRETTY_FUNCTION__ << "(" << 1*h << "," << 1*s << "," << 1*l << "," << 1*a << ")\n";
                return {h,s,l,a}; }
        };
    
        static inline std::ostream& operator<<(std::ostream& os, color const& c) {
            return os << "rgba(" << 1*c.red << "," << 1*c.green << "," << 1*c.blue << "," << 1*c.alpha << ")";
        }
    }
    
    BOOST_FUSION_ADAPT_STRUCT(ast::color, red, green, blue, alpha);
    
    namespace {
        namespace detail {
            template <class F, class Sequence, std::size_t... I>
                constexpr decltype(auto) apply_impl(F&& f, Sequence&& t, std::index_sequence<I...>)
                {
                    return std::invoke(std::forward<F>(f), boost::fusion::at_c<I>(std::forward<Sequence>(t))...);
                }
        }
    
        template <class F, class Sequence>
            constexpr decltype(auto) my_apply(F&& f, Sequence&& t)
            {
                return detail::apply_impl(
                        std::forward<F>(f), std::forward<Sequence>(t),
                        std::make_index_sequence<typename boost::fusion::result_of::size<std::remove_reference_t<Sequence> >::type{}>{});
            }
    }
    
    namespace parser {
        using namespace x3;
    
        constexpr auto uint8  = uint_parser<std::uint8_t>{};
    
        auto factory = [](auto f) {
            return [f](auto& ctx) {
                x3::_val(ctx) = my_apply(f, x3::_attr(ctx));
            };
        };
    
        const auto color_rgb  = rule<struct rgb, ast::color>{"rgb"}
                              = lit("rgb") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> x3::attr(255u) >> ')';
        const auto color_rgba = rule<struct rgba, ast::color>{"rgba"}
                              = lit("rgba") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> ')';
        const auto color_hsl  = rule<struct hsl, ast::color>{"hsl"}
                              = (lit("hsl") >> '(' >> uint8 >> ',' >> uint8 >> ',' >> uint8 >> attr(255u) >> ')') [factory(ast::color::hsl)];
    
        const auto color = skip(space) [ color_rgba | color_rgb | color_hsl ];
    } 
    
    int main() {
        for (std::string const input : {
                "rgb(1,2,3)",
                "rgba(4,5,6,7)",
                "hsl(8,9,10)" }) 
        {
            std::cout << " ----- Parsng " << std::quoted(input) << " --------\n";
            auto begin = input.begin(), end = input.end();
    
            ast::color result;
            bool success = parse(begin, end, parser::color, result);
    
            if (success) {
                std::cout << "parsed: ";
                std::cout << result << "\n";
            } else {
                std::cout << "failed\n";
            }
    
            if (begin != end) {
                std::cout << "Remaining unparsed: " << std::quoted(std::string(begin, end)) << std::endl;
            }
        }
    }
    

    ¹ it seems not to exist. Of course you could copy to a std::tuple and use std::apply (also experimental)