djowel / spirit_x3

Spirit X3 Experimental
147 stars 33 forks source link

enhancement: allow variable templates to circumvent BOOST_SPIRIT_DEFINE #17

Open mfornace opened 8 years ago

mfornace commented 8 years ago

In my current work, I was getting a little tired of having to use BOOST_SPIRIT_DEFINE on all my rules / definitions. To get around it, I added a little hack into the X3 code:

template <class T, class=void> struct allow_default : std::true_type {};

and edited the template signature of the default parse_rule() to

template <typename ID, typename Attribute, typename Iterator, typename Context, typename ActualAttribute, std::enable_if_t<allow_default<ID>::value, int> = 0>

and in my own code write:

template <class T> static constexpr auto define = false; and template <typename ID, typename Attribute, class I, class C, class A, std::enable_if_t<(!is_same<decltype(define<ID>), bool const>), int> = 0> bool parse_rule(x3::rule<ID, Attribute> rule, I& first, I const &last , C const &context, A &attr) { static auto const def = (rule = define<ID>); return def.parse(first, last, context, boost::spirit::x3::unused, attr); }

namespace boost {namespace spirit { namespace x3 { template <class T> struct allow_default<T, std::enable_if_t<(!std::is_same<decltype(my_code::define<T>), bool const>) >> : std::false_type {}; }}}

With that in place I can just write the helper macro:

`

define DEF(x) template <> static auto const define<typename std::decay_t<decltype(x)>::id>

`

and then write e.g.

DEF(executable_program) = +program_unit;

This way I don't need to do BOOST_SPIRIT_DEFINE anywhere; the implementation of the rule is found by explicit variable template specialization (thus C++14 only I believe). I would prefer type template specialization, but I think it's not as clean in this case.

This approach would be simpler if there was support in X3 for user specialization to avoid the BOOST_SPIRIT_DEFINE avenue. Also, it seems like other C++14 people might like it more since it gets rid of the extra tying-together step. Plus this approach is not as macro-based (my DEF is just a little alias).

I may have overlooked some issue...but it seems to compile and work all right for me. If so, maybe it could be an enhancement? Of course I'm not recommending the exact approach I take above, just the general idea.

djowel commented 8 years ago

I was thinking about this strategy! So you beat me to it :) Anyway, before I can look at the code, could you please show the calculator example ported to using this scheme?

mfornace commented 8 years ago

Thanks. Yes, I could look into that. Are you referring to something like spirit_x3/example/x3/calc6.cpp? There are a bunch of calculator things in there.

djowel commented 8 years ago

Perhaps start with the simplest calculator.

mfornace commented 8 years ago

Here is the more-or-less exact replica of what I used in my own code, for "calc1.cpp". The main thing I personally needed in X3 was an easy way to specialize parse_rule. Below, I just hack an enable_if struct into the X3 code, then I use a variable template in my own code. That's probably fine for people who live in template heavy code already.

As far as general users go, I don't know the best way to do this. One way would be to register the namespace explicitly: something where X3_REGISTER_NAMESPACE(client, calculator_grammar) expands out to the equivalent of "MAIN CODE ADDITIONS FOR USER" section.

As far as I can tell, the variable template must be declared somewhere nested in the user namespace. And I don't think you can re-declare the template because variable templates always need initialization. Maybe there is something clever to get around this with ADL, I don't know.

Add this at top of "nonterminal/detail/rule.hpp":

namespace boost { namespace spirit { namespace x3 {
     template <class T, class=void> struct allow_default : std::true_type {};
}

Change parse_rule template signature at line ~48 of "nonterminal/detail/rule.hpp" to:

// default parse_rule implementation
template <typename ID, typename Attribute, typename Iterator, typename Context, 
   typename ActualAttribute, std::enable_if_t<allow_default<ID>::value, int> = 0>

Change parse_rule template signature at line ~28 of "nonterminal/rule.hpp" to:

// default parse_rule implementation
template <typename ID, typename Attribute, typename Iterator, typename Context, 
    typename ActualAttribute, std::enable_if_t<allow_default<ID>::value, int>>

Calculator example (calc1.cpp) is then as follows:

/*=============================================================================
    Copyright (c) 2001-2014 Joel de Guzman
    Distributed under the Boost Software License, Version 1.0. (See accompanying
    file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
=============================================================================*/
///////////////////////////////////////////////////////////////////////////////
//
//  Plain calculator example demonstrating the grammar. The parser is a
//  syntax checker only and does not do any semantic evaluation.
//
//  [ JDG May 10, 2002 ]        spirit 1
//  [ JDG March 4, 2007 ]       spirit 2
//  [ JDG February 21, 2011 ]   spirit 2.5
//  [ JDG June 6, 2014 ]        spirit x3
//
///////////////////////////////////////////////////////////////////////////////

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

#include <iostream>
#include <string>
#include <list>
#include <numeric>

namespace x3 = boost::spirit::x3;

// MAIN CODE ADDITIONS FOR USER IN THE FOLLOWING SECTION
/*******************************************************************************/
namespace client{ namespace calculator_grammar {
    // Variable template that defaults to unusable definition
    template <class T> static auto constexpr define = false;

    // Helper macro for shorter declaration...
#define X3_DEFINE(rule) template <> static auto const define<typename std::decay_t<decltype(rule)>::id>

    // This function will take over from old one for cases where define is specialized
    template <typename ID, typename Attribute, class I, class C, class A,
        std::enable_if_t<(!std::is_same<decltype(define<ID>), bool const>::value), int> = 0>
    bool parse_rule(x3::rule<ID, Attribute> rule, I& first, I const &last , C const &context, A &attr) {
        static auto const def = (rule = define<ID>);
        return def.parse(first, last, context, x3::unused, attr);
    }
}}

namespace boost { namespace spirit {namespace x3 {
    // Helper type to give some error if trying to use an undefined rule
    template <class T>
    struct allow_default<T, std::enable_if_t<(!std::is_same<decltype(client::calculator_grammar::define<T>), bool const>::value)>> : std::false_type {};
}}}
/*******************************************************************************/

namespace client
{
    ///////////////////////////////////////////////////////////////////////////////
    //  The calculator grammar
    ///////////////////////////////////////////////////////////////////////////////
    namespace calculator_grammar
    {
        using x3::uint_;
        using x3::char_;

        x3::rule<class expression> const expression("expression");
        x3::rule<class term> const term("term");
        x3::rule<class factor> const factor("factor");

        // NOTHING DIFFERENT EXCEPT MACRO USE AND NO BOOST_SPIRIT_DEFINE

        X3_DEFINE(expression) =
            term
            >> *(   ('+' >> term)
                |   ('-' >> term)
                )
            ;

        X3_DEFINE(term) =
            factor
            >> *(   ('*' >> factor)
                |   ('/' >> factor)
                )
            ;

        X3_DEFINE(factor) =
                uint_
            |   '(' >> expression >> ')'
            |   ('-' >> factor)
            |   ('+' >> factor)
            ;

        auto calculator = expression;
    }

    using calculator_grammar::calculator;

}

///////////////////////////////////////////////////////////////////////////////
//  Main program
///////////////////////////////////////////////////////////////////////////////
int
main()
{
    std::cout << "/////////////////////////////////////////////////////////\n\n";
    std::cout << "Expression parser...\n\n";
    std::cout << "/////////////////////////////////////////////////////////\n\n";
    std::cout << "Type an expression...or [q or Q] to quit\n\n";

    typedef std::string::const_iterator iterator_type;

    std::string str;
    while (std::getline(std::cin, str))
    {
        if (str.empty() || str[0] == 'q' || str[0] == 'Q')
            break;

        auto& calc = client::calculator;    // Our grammar

        iterator_type iter = str.begin();
        iterator_type end = str.end();
        boost::spirit::x3::ascii::space_type space;
        bool r = phrase_parse(iter, end, calc, space);

        if (r && iter == end)
        {
            std::cout << "-------------------------\n";
            std::cout << "Parsing succeeded\n";
            std::cout << "-------------------------\n";
        }
        else
        {
            std::string rest(iter, end);
            std::cout << "-------------------------\n";
            std::cout << "Parsing failed\n";
            std::cout << "stopped at: \"" << rest << "\"\n";
            std::cout << "-------------------------\n";
        }
    }

    std::cout << "Bye... :-) \n\n";
    return 0;
}

Let me know if that makes sense/works. It works on my computer, but I could have forgotten something here...

mfornace commented 8 years ago

So I got a version working without as much user junk, if you're interested. It is rather a lot of junk in the macro, and some more complicated templating. But here goes:

The strategy is to find the user declared definition via ADL. To do this, I add a forward declared function into the X3_DEFINE macro which returns a class type. The class type is a specialization of a template class for the given rule ID. The template class is declared repeatedly in each macro since it is forward declaration. The class contains a templated variable member, with template parameter bool to delay the definition to later. Then we do explicit specialization of of this template variable member for "true" ("false" is never used) to end the macro.

Caveats: the template ID needs to be in the same or nested namespace for the ADL to work, I think. Actually I guess I could have used the rule type rather than the ID to get around this, but I will not rewrite my code just for that :D. I think that would be better though. --edit: oh, maybe not since the rule type is in x3 namespace.

IMPLEMENTATION:

Somewhere in X3 (I did top of "nonterminal/detail/rule.hpp") add this macro

// Macro explanation:
// this can be declared repeatedly wherever since it's just forward declare
// specialize the struct to contain a definition member which is templated on bool (never encounter with bool=false)
// let X3 find the struct via ADL
// specialize the struct's member definition for case(true): I thought I needed template<> template<> but apparently not.

#define X3_DEFINE(rule) \
    template <class T> struct define_t; \
    template <> struct define_t<typename std::decay_t<decltype(rule)>::id> {template <bool=true> static constexpr auto definition = false;}; \
    define_t<typename std::decay_t<decltype(rule)>::id> get_define_t(typename std::decay_t<decltype(rule)>::id *); \
    template <> auto const define_t<typename std::decay_t<decltype(rule)>::id>::definition<>

At the top of "nonterminal/detail/rule.hpp" add this in X3 namespace:

// Default case which should disable custom use
int get_define_t(...);

// Detect the custom definition: default false
template <class ID, class=void> struct has_custom_definition : std::false_type {};
// Detect the custom definition: look at declared return type of get_define_t(ID *). Resort to pointer, not declval<>() because incomplete type error I got
template <class ID> struct has_custom_definition<ID, std::enable_if_t<(!std::is_same<int, decltype(get_define_t((ID *) nullptr))>::value)>> : std::true_type {};

// This function will take over from old one for cases where define_t is specialized
template <typename ID, typename Attribute, class I, class C, class A, std::enable_if_t<has_custom_definition<ID>::value, int> = 0>
bool parse_rule(x3::rule<ID, Attribute> rule, I& first, I const &last , C const &context, A &attr) {
    static auto const def = (rule = decltype(get_define_t((ID *) nullptr))::template definition<>);
    return def.parse(first, last, context, x3::unused, attr);
}

Change the default "parse_rule" template signatures (in same places as before) to:

template <typename ID, typename Attribute, typename Iterator
      , typename Context, typename ActualAttribute, std::enable_if_t<!has_custom_definition<ID>::value, int> = 0>

and

template <typename ID, typename Attribute, typename Iterator
      , typename Context, typename ActualAttribute, std::enable_if_t<!has_custom_definition<ID>::value, int>>

Finally, the user file is now stripped of anything but X3_DEFINE:

/*=============================================================================
    Copyright (c) 2001-2014 Joel de Guzman
    Distributed under the Boost Software License, Version 1.0. (See accompanying
    file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
=============================================================================*/
///////////////////////////////////////////////////////////////////////////////
//
//  Plain calculator example demonstrating the grammar. The parser is a
//  syntax checker only and does not do any semantic evaluation.
//
//  [ JDG May 10, 2002 ]        spirit 1
//  [ JDG March 4, 2007 ]       spirit 2
//  [ JDG February 21, 2011 ]   spirit 2.5
//  [ JDG June 6, 2014 ]        spirit x3
//
///////////////////////////////////////////////////////////////////////////////

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

#include <iostream>
#include <string>
#include <list>
#include <numeric>

namespace x3 = boost::spirit::x3;

namespace client
{
    ///////////////////////////////////////////////////////////////////////////////
    //  The calculator grammar
    ///////////////////////////////////////////////////////////////////////////////
    namespace calculator_grammar
    {
        using x3::uint_;
        using x3::char_;

        class expression_t;
        x3::rule<expression_t> const expression("expression");
        x3::rule<class term> const term("term");
        x3::rule<class factor> const factor("factor");

        X3_DEFINE(expression) =
            term
            >> *(   ('+' >> term)
                |   ('-' >> term)
                )
            ;

        X3_DEFINE(term) =
            factor
            >> *(   ('*' >> factor)
                |   ('/' >> factor)
                )
            ;

        X3_DEFINE(factor) =
                uint_
            |   '(' >> expression >> ')'
            |   ('-' >> factor)
            |   ('+' >> factor)
            ;

        auto calculator = expression;
    }

    using calculator_grammar::calculator;

}

///////////////////////////////////////////////////////////////////////////////
//  Main program
///////////////////////////////////////////////////////////////////////////////
int
main()
{
    std::cout << "/////////////////////////////////////////////////////////\n\n";
    std::cout << "Expression parser...\n\n";
    std::cout << "/////////////////////////////////////////////////////////\n\n";
    std::cout << "Type an expression...or [q or Q] to quit\n\n";

    typedef std::string::const_iterator iterator_type;

    std::string str;
    while (std::getline(std::cin, str))
    {
        if (str.empty() || str[0] == 'q' || str[0] == 'Q')
            break;

        auto& calc = client::calculator;    // Our grammar

        iterator_type iter = str.begin();
        iterator_type end = str.end();
        boost::spirit::x3::ascii::space_type space;
        bool r = phrase_parse(iter, end, calc, space);

        if (r && iter == end)
        {
            std::cout << "-------------------------\n";
            std::cout << "Parsing succeeded\n";
            std::cout << "-------------------------\n";
        }
        else
        {
            std::string rest(iter, end);
            std::cout << "-------------------------\n";
            std::cout << "Parsing failed\n";
            std::cout << "stopped at: \"" << rest << "\"\n";
            std::cout << "-------------------------\n";
        }
    }

    std::cout << "Bye... :-) \n\n";
    return 0;
}