2
0
mirror of https://github.com/boostorg/spirit.git synced 2026-01-19 04:42:11 +00:00

Modernize x3::action (#815)

`x3::action` now uses concepts to dispatch `f(ctx)` or `f()`, depending on 
whether `f` accepts such signature.

The dispatching implementation is moved from `call.hpp` to `action.hpp`,
 as the function is only used from the `x3::action` class.

`operator/`: Deprecated. The symbol `/` normally means "ordered choice"
in PEG, and is irrelevant to semantic actions. Furthermore, using C++'s 
`operator/` for this purpose may introduce surprising behavior when it's 
mixed with ordinary PEG operators, for instance, the unary `operator+`, 
due to precedence.
This commit is contained in:
Nana Sakisaka
2025-09-10 07:55:14 +09:00
committed by GitHub
parent bae393d159
commit 3df9ca3788
3 changed files with 248 additions and 136 deletions

View File

@@ -1,5 +1,6 @@
/*=============================================================================
Copyright (c) 2001-2014 Joel de Guzman
Copyright (c) 2025 Nana Sakisaka
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)
@@ -9,102 +10,243 @@
#include <boost/spirit/home/x3/support/context.hpp>
#include <boost/spirit/home/x3/support/traits/attribute_of.hpp>
#include <boost/spirit/home/x3/core/call.hpp>
#include <boost/spirit/home/x3/nonterminal/detail/transform_attribute.hpp>
#include <boost/range/iterator_range_core.hpp>
#include <boost/spirit/home/x3/core/call.hpp>
namespace boost { namespace spirit { namespace x3
#ifndef BOOST_SPIRIT_X3_NO_BOOST_ITERATOR_RANGE
# pragma message("Use of `boost::iterator_range` is deprecated in X3. #define BOOST_SPIRIT_X3_NO_BOOST_ITERATOR_RANGE")
# include <boost/range/iterator_range_core.hpp>
#endif
#include <ranges>
#include <iterator>
#include <concepts>
#include <type_traits>
#include <utility>
namespace boost::spirit::x3
{
struct raw_attribute_type;
struct parse_pass_context_tag;
struct raw_attribute_type; // TODO: move this to detail
template <typename Context>
inline bool& _pass(Context const& context)
{
return x3::get<parse_pass_context_tag>(context);
}
// Ideally we should have a context-agnostic concept that can be used
// like `X3ActionFunctor<F>`, but we technically can't.
//
// In order to check `std::invocable`, we need to know the actual context
// type passed to the `.parse(...)` function but it is unknown until
// runtime.
//
// Even if we make up the most trivial context type (i.e. `unused_type`),
// such concept will be useless because a user-provided functor always
// operates on user-specific precondition that assumes the context
// holds exact specific type provided to the entry point (`x3::parse`).
template <typename Subject, typename Action>
struct action : unary_parser<Subject, action<Subject, Action>>
{
typedef unary_parser<Subject, action<Subject, Action>> base_type;
static bool const is_pass_through_unary = true;
static bool const has_action = true;
static_assert(
!std::is_reference_v<Action>,
"Reference type is disallowed for semantic action functor to prevent dangling reference"
);
constexpr action(Subject const& subject, Action f)
: base_type(subject), f(f) {}
using base_type = unary_parser<Subject, action<Subject, Action>>;
static constexpr bool is_pass_through_unary = true;
static constexpr bool has_action = true;
template <typename Iterator, typename Context, typename RuleContext, typename Attribute>
bool call_action(
Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, Attribute& attr) const
Action f;
template <typename SubjectT, typename ActionT>
requires std::is_constructible_v<base_type, SubjectT> && std::is_constructible_v<Action, ActionT>
constexpr action(SubjectT&& subject, ActionT&& f)
noexcept(std::is_nothrow_constructible_v<base_type, SubjectT> && std::is_nothrow_constructible_v<Action, ActionT>)
: base_type(std::forward<SubjectT>(subject))
, f(std::forward<ActionT>(f))
{
bool pass = true;
auto action_context = make_context<parse_pass_context_tag>(pass, context);
call(f, first, last, action_context, rcontext, attr);
return pass;
}
template <typename Iterator, typename Context
, typename RuleContext, typename Attribute>
bool parse_main(Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, Attribute& attr) const
{
Iterator save = first;
if (this->subject.parse(first, last, context, rcontext, attr))
{
if (call_action(first, last, context, rcontext, attr))
return true;
// reset iterators if semantic action failed the match
// retrospectively
first = save;
}
return false;
}
// attr==raw_attribute_type, action wants iterator_range (see raw.hpp)
template <typename Iterator, typename Context, typename RuleContext>
bool parse_main(Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, raw_attribute_type&) const
{
boost::iterator_range<Iterator> rng;
// synthesize the attribute since one is not supplied
return parse_main(first, last, context, rcontext, rng);
}
// attr==unused, action wants attribute
template <typename Iterator, typename Context, typename RuleContext>
bool parse(Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, unused_type) const
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext>
[[nodiscard]] constexpr bool
parse(
It& first, Se const& last, Context const& context, RContext& rcontext, unused_type
) const noexcept(
std::is_nothrow_default_constructible_v<traits::attribute_of_t<action<Subject, Action>, Context>> &&
noexcept(this->parse_main(first, last, context, rcontext, std::declval<traits::attribute_of_t<action<Subject, Action>, Context>&>()))
)
{
typedef typename
traits::attribute_of<action<Subject, Action>, Context>::type
attribute_type;
using attribute_type = traits::attribute_of_t<action<Subject, Action>, Context>;
// Synthesize the attribute since one is not supplied
attribute_type attribute; // default-initialize
return this->parse_main(first, last, context, rcontext, attribute);
}
// Catch-all overload for non-unused_type attribute
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
[[nodiscard]] constexpr bool
parse(
It& first, Se const& last, Context const& context, RContext& rcontext, Attribute& attr
) const noexcept(noexcept(this->parse_main(first, last, context, rcontext, attr)))
{
return this->parse_main(first, last, context, rcontext, attr);
}
private:
// Compose attr(where(val(pass(context))))
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
using composed_context_t = x3::context<
attr_context_tag,
Attribute,
x3::context<
where_context_tag,
#ifdef BOOST_SPIRIT_X3_NO_BOOST_ITERATOR_RANGE
std::ranges::subrange<It, Se> const,
#else
boost::iterator_range<It> const,
#endif
x3::context<
rule_val_context_tag,
RContext,
x3::context<
parse_pass_context_tag,
bool,
Context
>
>
>
>;
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
requires std::invocable<Action const&, composed_context_t<It, Se, Context, RContext, Attribute> const&>
[[nodiscard]] constexpr bool
call_action(
It& first, Se const& last,
Context const& context, RContext& rcontext, Attribute& attr
) const noexcept(false) // construction of `subrange` is never noexcept as per the standard
{
#ifdef BOOST_SPIRIT_X3_NO_BOOST_ITERATOR_RANGE
using where_range_t = std::ranges::subrange<It, Se>;
#else
using where_range_t = boost::iterator_range<It>;
#endif
static_assert(
std::is_void_v<std::invoke_result_t<Action const&, composed_context_t<It, Se, Context, RContext, Attribute> const&>>,
"Semantic action should not return value. Check your function signature."
);
bool pass = true;
auto const pass_context = x3::make_context<parse_pass_context_tag>(pass, context);
auto const val_context = x3::make_context<rule_val_context_tag>(rcontext, pass_context);
// TODO: Provide some trait to detect whether this is actually needed for
// each semantic actions.
//
// Although this can be assumed to be eliminated in optimized code,
// this still may introduce compile time overhead (and also runtime
// overhead, as constructing `subrange` is never noexcept).
where_range_t const where_rng(first, last);
auto const where_context = x3::make_context<where_context_tag>(where_rng, val_context);
auto const attr_context = x3::make_context<attr_context_tag>(attr, where_context);
// Sanity check (internal check to detect implementation divergence)
static_assert(std::same_as<
std::remove_cvref_t<decltype(attr_context)>,
composed_context_t<It, Se, Context, RContext, Attribute>
>);
this->f(attr_context);
return pass;
}
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
requires (!std::invocable<Action const&, composed_context_t<It, Se, Context, RContext, Attribute> const&>)
[[nodiscard]] constexpr bool
call_action(
It&, Se const&,
Context const&, RContext&, Attribute&
) const noexcept(std::is_nothrow_invocable_v<Action const&>)
{
// Explicitly make this hard error instead of emitting "no matching overload".
// This provides much more human-friendly errors.
static_assert(
std::invocable<Action const&>,
"Neither `f(ctx)` nor `f()` is well-formed for your semantic action. "
"Check your function signature. Note that some functors might need "
"`const` qualifier to satisfy the constraints."
);
static_assert(
std::is_void_v<std::invoke_result_t<Action const&>>,
"Semantic action should not return value. Check your function signature."
);
this->f();
return true;
}
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext, typename Attribute>
[[nodiscard]] constexpr bool
parse_main(
It& first, Se const& last, Context const& context, RContext& rcontext, Attribute& attr
) const noexcept(
std::is_copy_assignable_v<It> &&
is_nothrow_parsable_v<Subject, It, Se, Context, RContext, Attribute> &&
noexcept(this->call_action(first, last, context, rcontext, attr))
)
{
It const saved_first = first;
if (this->subject.parse(first, last, context, rcontext, attr))
{
if (this->call_action(first, last, context, rcontext, attr))
{
return true;
}
// reset iterators if semantic action failed the match
// retrospectively
first = saved_first;
}
return false;
}
// attr==raw_attribute_type, action wants iterator_range (see raw.hpp)
template <std::forward_iterator It, std::sentinel_for<It> Se, typename Context, typename RContext>
[[nodiscard]] constexpr bool
parse_main(
It& first, Se const& last, Context const& context, RContext& rcontext, raw_attribute_type&
) const noexcept(false) // construction of `subrange` is never noexcept as per the standard
{
#ifdef BOOST_SPIRIT_X3_NO_BOOST_ITERATOR_RANGE
std::ranges::subrange<It, Se> rng;
#else
boost::iterator_range<It> rng;
#endif
// synthesize the attribute since one is not supplied
attribute_type attribute{};
return parse_main(first, last, context, rcontext, attribute);
return this->parse_main(first, last, context, rcontext, rng);
}
// main parse function
template <typename Iterator, typename Context
, typename RuleContext, typename Attribute>
bool parse(Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, Attribute& attr) const
{
return parse_main(first, last, context, rcontext, attr);
}
Action f;
};
template <typename P, typename Action>
constexpr action<typename extension::as_parser<P>::value_type, Action>
operator/(P const& p, Action f)
template <X3Subject Subject, typename Action>
[[nodiscard, deprecated(
"Use `operator[]` instead. The symbol `/` normally means \"ordered choice\" "
"in PEG, and is irrelevant to semantic actions. Furthermore, using C++'s "
"`operator/` for this purpose may introduce surprising behavior when it's "
"mixed with ordinary PEG operators, for instance, the unary `operator+`, "
"due to precedence."
)]]
constexpr action<as_parser_plain_t<Subject>, std::remove_cvref_t<Action>>
operator/(Subject&& p, Action&& f)
noexcept(
is_parser_nothrow_castable_v<Subject> &&
std::is_nothrow_constructible_v<
action<as_parser_plain_t<Subject>, std::remove_cvref_t<Action>>,
as_parser_t<Subject>, Action
>
)
{
return { as_parser(p), f };
return { as_parser(std::forward<Subject>(p)), std::forward<Action>(f) };
}
}}}
} // boost::spirit::x3
#endif

View File

@@ -1,5 +1,6 @@
/*=============================================================================
Copyright (c) 2001-2014 Joel de Guzman
Copyright (c) 2025 Nana Sakisaka
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)
@@ -7,70 +8,38 @@
#if !defined(BOOST_SPIRIT_X3_CALL_CONTEXT_MAY_26_2014_0234PM)
#define BOOST_SPIRIT_X3_CALL_CONTEXT_MAY_26_2014_0234PM
#include <type_traits>
#include <boost/spirit/home/x3/support/context.hpp>
#include <boost/spirit/home/x3/support/utility/is_callable.hpp>
#include <boost/range/iterator_range_core.hpp>
namespace boost { namespace spirit { namespace x3
namespace boost::spirit::x3
{
////////////////////////////////////////////////////////////////////////////
struct rule_val_context_tag;
struct parse_pass_context_tag; // _pass
struct rule_val_context_tag; // _val
struct where_context_tag; // _where
struct attr_context_tag; // _attr
template <typename Context>
inline decltype(auto) _val(Context const& context)
[[nodiscard]] constexpr bool& _pass(Context const& context) noexcept
{
return x3::get<parse_pass_context_tag>(context);
}
template <typename Context>
[[nodiscard]] constexpr auto&& _val(Context const& context) noexcept
{
return x3::get<rule_val_context_tag>(context);
}
////////////////////////////////////////////////////////////////////////////
struct where_context_tag;
template <typename Context>
inline decltype(auto) _where(Context const& context)
[[nodiscard]] constexpr auto&& _where(Context const& context) noexcept
{
return x3::get<where_context_tag>(context);
}
////////////////////////////////////////////////////////////////////////////
struct attr_context_tag;
template <typename Context>
inline decltype(auto) _attr(Context const& context)
[[nodiscard]] constexpr auto&& _attr(Context const& context) noexcept
{
return x3::get<attr_context_tag>(context);
}
////////////////////////////////////////////////////////////////////////////
namespace detail
{
template <typename F, typename Context>
auto call(F f, Context const& context, mpl::true_)
{
return f(context);
}
template <typename F, typename Context>
auto call(F f, Context const& /* context */, mpl::false_)
{
return f();
}
}
template <
typename F, typename Iterator
, typename Context, typename RuleContext, typename Attribute>
auto call(
F f, Iterator& first, Iterator const& last
, Context const& context, RuleContext& rcontext, Attribute& attr)
{
boost::iterator_range<Iterator> rng(first, last);
auto val_context = make_context<rule_val_context_tag>(rcontext, context);
auto where_context = make_context<where_context_tag>(rng, val_context);
auto attr_context = make_context<attr_context_tag>(attr, where_context);
return detail::call(f, attr_context, is_callable<F(decltype(attr_context) const&)>());
}
}}}
} // boost::spirit::x3
#endif

View File

@@ -13,6 +13,7 @@
#include <boost/fusion/include/std_pair.hpp>
#include <boost/variant.hpp>
#include <string>
#include <vector>
#include <cstring>
@@ -108,15 +109,14 @@ int main()
using spirit_test::test_attr;
using spirit_test::test;
using namespace boost::spirit::x3::ascii;
using namespace boost::spirit::x3::standard;
using boost::spirit::x3::rule;
using boost::spirit::x3::lit;
using boost::spirit::x3::eps;
using boost::spirit::x3::unused_type;
{ // synth attribute value-init
// synth attribute value-init
{
std::string s;
typedef rule<class r, std::string> rule_type;
@@ -128,17 +128,15 @@ int main()
BOOST_TEST(s == "abcdef");
}
{ // synth attribute value-init
// synth attribute value-init
{
std::string s;
typedef rule<class r, std::string> rule_type;
auto rdef = rule_type() =
alpha /
[](auto& ctx)
{
_val(ctx) += _attr(ctx);
}
alpha[([](auto& ctx) {
_val(ctx) += _attr(ctx);
})]
;
BOOST_TEST(test_attr("abcdef", +rdef, s));
@@ -148,13 +146,16 @@ int main()
{
auto r = rule<class r_id, int>{} = eps[([] (auto& ctx) {
using boost::spirit::x3::_val;
static_assert(std::is_same<std::decay_t<decltype(_val(ctx))>, unused_type>::value,
"Attribute must not be synthesized");
static_assert(
std::is_same_v<std::decay_t<decltype(_val(ctx))>, unused_type>,
"Attribute must not be synthesized"
);
})];
BOOST_TEST(test("", r));
}
{ // ensure no unneeded synthesization, copying and moving occurred
// ensure no unneeded synthesization, copying and moving occurred
{
stationary st { 0 };
BOOST_TEST(test_attr("{42}", check_stationary::b, st));
BOOST_TEST_EQ(st.val, 42);