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

Boost X3. Get rid of identity lambda to propagate attribute to value


I'm migrating my code from Boost Qi to X3. I have non-terminal rule expression that is compiling to structure ast::Expression. For the minimal reproducing example I left 2 statements that forms expression. value creates new while '(' expression ')' should allow use brackets and just propogate result of expression to parent definition.

In the Qi I had %= operator that copied attribute to value. So the question - how to get rid of lambda e2e in the following code?

#include <iostream>
#include <tuple>
#include <string>
#include <variant>
#include <vector>

#include <boost/spirit/home/x3.hpp>
#include <boost/spirit/home/x3/support/ast/variant.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/fusion/adapted/std_tuple.hpp>

namespace x3 = boost::spirit::x3;

namespace ast
{
    enum class Cmd  : int
    {
        const_val = 42
    };
    using data_t = x3::variant<int, double>;
    struct Expression
    {
        Cmd _cmd;
        std::string _args;
    };
}//ast

BOOST_FUSION_ADAPT_STRUCT(ast::Expression, _cmd, _args)



inline std::ostream& operator << (std::ostream& os, const ast::Expression& val)
{
    os << "{" << (int)val._cmd << ':' << val._args << "}";
    return os;
}


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

    x3::rule<class expression, ast::Expression> const expression("expression_stmt");
    x3::rule<class value, ast::data_t> const value("value_stmt");

    auto const value_def =
        x3::int_
        | x3::double_
        ;

    // construct expression form plain constant
    auto val_to_expr = [](auto& ctx) {
        auto& a1 = _attr(ctx);
        _val(ctx) = ast::Expression{ 
            ast::Cmd::const_val, 
            std::to_string(boost::get<int>(a1)) 
        };
        };

    // copy attribute to result value
    auto e2e = [](auto& ctx) {
        auto& a1 = _attr(ctx);
        _val(ctx) = a1;
        };

    auto const expression_def =
        value[val_to_expr]
// !!!! why I need e2e to copy attr to value?
        | ('(' > expression > ')') [e2e]
        ;

    BOOST_SPIRIT_DEFINE(expression, value);
}//ns:client

auto parse_text(const std::string& s)
{

    namespace x3 = boost::spirit::x3;
    auto iter = s.cbegin();
    auto end_iter = s.cend();

    ast::Expression res;
    x3::ascii::space_type space;

    bool success = x3::phrase_parse(iter, end_iter,
        client::expression, space,
        res);
    if (success && iter == end_iter)
    {
        std::cout << "Success\n";
    }
    else {
        std::cout << "Parse failure\n{";
        while (iter != end_iter)
            std::cout << *iter++;
        std::cout << "}\n";
    }
    return res;
}

int main()
{
    auto test = +[](const std::string& s) {
            std::cout << "parsing " << s << '\n';
            auto res = parse_text(s);
            std::cout << "'" << res << "'\n";
        };
    ;
    test("123");
    test("(123)");
}

Solution

  • You can set the force_attribute template argument on x3::rule:

    x3::rule<class expression, ast::Expression, true> const expression{"expression"};
    

    However, this means that all attribute assignments will be forced, so you have to facilitate val_to_expr. You easily can, using the relevant constructor:

    struct Expression {
        Cmd         _cmd;
        std::string _args;
    
        Expression(value_t v = {}) //
            : _cmd(Cmd::const_val)
            , _args(std::to_string(get<int>(v))) {}
    };
    

    Also, in order for the constructor to be used, you should remove the Fusion adaptation, which you weren't using anyways. Now everything fits in roughly half the size of the code:

    Live On Coliru

    #include <boost/spirit/home/x3.hpp>
    #include <boost/spirit/home/x3/support/ast/variant.hpp>
    #include <iomanip>
    #include <iostream>
    #include <optional>
    
    namespace x3 = boost::spirit::x3;
    
    namespace ast {
        using boost::get;
        enum class Cmd : int { const_val = 42 };
        using value_t = x3::variant<int, double>;
    
        struct Expression {
            Cmd         _cmd;
            std::string _args;
    
            Expression(value_t v = {}) //
                : _cmd(Cmd::const_val)
                , _args(std::to_string(get<int>(v))) {}
    
            friend std::ostream& operator<<(std::ostream& os, Expression const& e) {
                os << "{" << static_cast<int>(e._cmd) << ':' << e._args << "}";
                return os;
            }
        };
    } // namespace ast
    
    namespace client {
        x3::rule<class expression, ast::Expression, true> const expression{"expression"};
        x3::rule<class value, ast::value_t> const               value{"value"};
    
        auto const value_def      = x3::int_ | x3::double_;
        auto const expression_def = value | ('(' > expression > ')');
    
        BOOST_SPIRIT_DEFINE(expression, value)
    } // namespace client
    
    std::optional<ast::Expression> parse_text(std::string_view s) {
        if (ast::Expression res;
            phrase_parse(s.cbegin(), s.cend(), client::expression >> x3::eoi, x3::space, res))
            return res;
        else
            return std::nullopt;
    }
    
    int main() {
        for (std::string_view s : {"123", "(123)"})
            if (auto e = parse_text(s))
                std::cout << quoted(s) << " -> " << *e << "\n";
            else
                std::cout << quoted(s) << " failed\n";
    }
    

    Printing

    "123" -> {42:123}
    "(123)" -> {42:123}
    

    Bonus/Caveat

    1. Ironically, at the end of that, you no longer need force_attribute to begin with, because you don't use any semantic actions (see Boost Spirit: "Semantic actions are evil"?).

    2. Also, the production int_ | double_ will never match doubles (see e.g. Why doesn't this boost::spirit::qi rule successfully parse?).

    3. It looks as though you're crafting the classical expression parser. As such, you'd very much expect values to be expressions:

      struct Command {
          enum Code : int { const_val = 42 };
          Code        _cmd;
          std::string _args;
      };
      
      using Expression = boost::variant<int, double, Command>;
      

      Now you can enjoy no-semantic-action Fusion adaptation bliss:

      x3::rule<class command, ast::Command>  const command{"command"};
      x3::rule<class expr,  ast::Expression> const expr{"expr"};
      
      auto dbl         = x3::real_parser<double, x3::strict_real_policies<double>>{};
      auto value       = dbl | x3::int_;
      auto command_def = '(' > x3::attr(ast::Command::const_val) > x3::raw[value % ','] > ')';
      auto expr_def    = command | value;
      

      Printing Live On Coliru:

      "123" -> 123
      "(123)" -> {42:123}
      
    4. Having reached this point, it would become more obvious that it is probably NOT necessary to put _args as a string. I bet you'd rather recurse with std::vector<Expression>?

      struct Command {
          enum Code : int { const_val = 42 };
          Code                    _cmd;
          std::vector<Expression> _args;
      };
      

      And a minor change of the Command rule:

      auto const command_def = '(' > x3::attr(ast::Command::const_val) > (expr % ',') > ')';
      

    Now you can parse arbitrarily nested expressions, which do not even flatten to strings in the AST:

    Live On Coliru

    // #define BOOST_SPIRIT_X3_DEBUG
    #include <boost/fusion/adapted.hpp>
    #include <boost/spirit/home/x3.hpp>
    #include <iomanip>
    #include <iostream>
    #include <optional>
    
    namespace x3 = boost::spirit::x3;
    
    namespace ast {
        struct Command;
        using Expression = boost::variant<int, double, boost::recursive_wrapper<Command>>;
    
        struct Command {
            enum Code : int { const_val = 42 };
            Code                    _cmd;
            std::vector<Expression> _args;
    
            friend std::ostream& operator<<(std::ostream& os, std::vector<Expression> const& ee) {
                for (auto sep ="["; auto& e : ee)
                    os << std::exchange(sep, ",") << e;
                return ee.empty() ? os : os << "]";
            }
            friend std::ostream& operator<<(std::ostream& os, Command const& e) {
                os << "{" << e._cmd << ':' << e._args << "}";
                return os;
            }
            friend std::ostream& operator<<(std::ostream& os, Code const& c) {
                switch (c) {
                    case const_val: return os << "const_val";
                    default:        return os << "?";
                }
            }
        };
    
    } // namespace ast
    
    BOOST_FUSION_ADAPT_STRUCT(ast::Command, _cmd, _args)
    
    namespace client {
        x3::rule<class command, ast::Command>  const command{"command"};
        x3::rule<class expr,  ast::Expression> const expr{"expr"};
    
        auto const dbl         = x3::real_parser<double, x3::strict_real_policies<double>>{};
        auto const value       = dbl | x3::int_;
        auto const command_def = '(' > x3::attr(ast::Command::const_val) > (expr % ',') > ')';
        auto const expr_def    = command | value;
    
        BOOST_SPIRIT_DEFINE(command, expr)
    } // namespace client
    
    std::optional<ast::Expression> parse_text(std::string_view s) {
        if (ast::Expression e;
            phrase_parse(s.cbegin(), s.cend(), client::expr >> x3::eoi, x3::space, e))
            return e;
        else
            return std::nullopt;
    }
    
    int main() {
        for (std::string_view s : {
                 "123",
                 "(123)",
                 "(123, 234, 345)",
                 "(123, 234, (345, 42.7e-3), 456)",
             })
            if (auto e = parse_text(s))
                std::cout << std::setw(34) << quoted(s) << " -> " << *e << "\n";
            else
                std::cout << std::setw(34) << quoted(s) << " failed\n";
    }
    

    Printing:

                                 "123" -> 123
                               "(123)" -> {const_val:[123]}
                     "(123, 234, 345)" -> {const_val:[123,234,345]}
     "(123, 234, (345, 42.7e-3), 456)" -> {const_val:[123,234,{const_val:[345,0.0427]},456]}