From 3df9ca3788dfec5f54730284b5d2cbb2d40472a3 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:55:14 +0900 Subject: [PATCH] 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. --- include/boost/spirit/home/x3/core/action.hpp | 292 ++++++++++++++----- include/boost/spirit/home/x3/core/call.hpp | 63 +--- test/x3/rule3.cpp | 29 +- 3 files changed, 248 insertions(+), 136 deletions(-) diff --git a/include/boost/spirit/home/x3/core/action.hpp b/include/boost/spirit/home/x3/core/action.hpp index 0de52c93b..4640ac00c 100644 --- a/include/boost/spirit/home/x3/core/action.hpp +++ b/include/boost/spirit/home/x3/core/action.hpp @@ -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 #include -#include #include -#include +#include -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 +#endif + +#include +#include +#include +#include +#include + +namespace boost::spirit::x3 { - struct raw_attribute_type; - struct parse_pass_context_tag; + struct raw_attribute_type; // TODO: move this to detail - template - inline bool& _pass(Context const& context) - { - return x3::get(context); - } + // Ideally we should have a context-agnostic concept that can be used + // like `X3ActionFunctor`, 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 struct action : unary_parser> { - typedef unary_parser> base_type; - static bool const is_pass_through_unary = true; - static bool const has_action = true; + static_assert( + !std::is_reference_v, + "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>; + static constexpr bool is_pass_through_unary = true; + static constexpr bool has_action = true; - template - bool call_action( - Iterator& first, Iterator const& last - , Context const& context, RuleContext& rcontext, Attribute& attr) const + Action f; + + template + requires std::is_constructible_v && std::is_constructible_v + constexpr action(SubjectT&& subject, ActionT&& f) + noexcept(std::is_nothrow_constructible_v && std::is_nothrow_constructible_v) + : base_type(std::forward(subject)) + , f(std::forward(f)) { - bool pass = true; - auto action_context = make_context(pass, context); - call(f, first, last, action_context, rcontext, attr); - return pass; - } - - template - 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 - bool parse_main(Iterator& first, Iterator const& last - , Context const& context, RuleContext& rcontext, raw_attribute_type&) const - { - boost::iterator_range rng; - // synthesize the attribute since one is not supplied - return parse_main(first, last, context, rcontext, rng); } // attr==unused, action wants attribute - template - bool parse(Iterator& first, Iterator const& last - , Context const& context, RuleContext& rcontext, unused_type) const + template 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, Context>> && + noexcept(this->parse_main(first, last, context, rcontext, std::declval, Context>&>())) + ) { - typedef typename - traits::attribute_of, Context>::type - attribute_type; + using attribute_type = traits::attribute_of_t, 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 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 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 const, + #else + boost::iterator_range const, + #endif + x3::context< + rule_val_context_tag, + RContext, + x3::context< + parse_pass_context_tag, + bool, + Context + > + > + > + >; + + template Se, typename Context, typename RContext, typename Attribute> + requires std::invocable 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; + #else + using where_range_t = boost::iterator_range; + #endif + + static_assert( + std::is_void_v const&>>, + "Semantic action should not return value. Check your function signature." + ); + + bool pass = true; + auto const pass_context = x3::make_context(pass, context); + + auto const val_context = x3::make_context(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_rng, val_context); + + auto const attr_context = x3::make_context(attr, where_context); + + // Sanity check (internal check to detect implementation divergence) + static_assert(std::same_as< + std::remove_cvref_t, + composed_context_t + >); + + this->f(attr_context); + return pass; + } + + template Se, typename Context, typename RContext, typename Attribute> + requires (!std::invocable const&>) + [[nodiscard]] constexpr bool + call_action( + It&, Se const&, + Context const&, RContext&, Attribute& + ) const noexcept(std::is_nothrow_invocable_v) + { + // Explicitly make this hard error instead of emitting "no matching overload". + // This provides much more human-friendly errors. + static_assert( + std::invocable, + "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>, + "Semantic action should not return value. Check your function signature." + ); + this->f(); + return true; + } + + template 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 && + is_nothrow_parsable_v && + 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 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 rng; + #else + boost::iterator_range 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 - 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 - constexpr action::value_type, Action> - operator/(P const& p, Action f) + template + [[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, std::remove_cvref_t> + operator/(Subject&& p, Action&& f) + noexcept( + is_parser_nothrow_castable_v && + std::is_nothrow_constructible_v< + action, std::remove_cvref_t>, + as_parser_t, Action + > + ) { - return { as_parser(p), f }; + return { as_parser(std::forward(p)), std::forward(f) }; } -}}} +} // boost::spirit::x3 #endif diff --git a/include/boost/spirit/home/x3/core/call.hpp b/include/boost/spirit/home/x3/core/call.hpp index a344cf4bf..74e7b46de 100644 --- a/include/boost/spirit/home/x3/core/call.hpp +++ b/include/boost/spirit/home/x3/core/call.hpp @@ -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 - #include -#include -#include -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 - inline decltype(auto) _val(Context const& context) + [[nodiscard]] constexpr bool& _pass(Context const& context) noexcept + { + return x3::get(context); + } + + template + [[nodiscard]] constexpr auto&& _val(Context const& context) noexcept { return x3::get(context); } - //////////////////////////////////////////////////////////////////////////// - struct where_context_tag; - template - inline decltype(auto) _where(Context const& context) + [[nodiscard]] constexpr auto&& _where(Context const& context) noexcept { return x3::get(context); } - //////////////////////////////////////////////////////////////////////////// - struct attr_context_tag; - template - inline decltype(auto) _attr(Context const& context) + [[nodiscard]] constexpr auto&& _attr(Context const& context) noexcept { return x3::get(context); } - - //////////////////////////////////////////////////////////////////////////// - namespace detail - { - template - auto call(F f, Context const& context, mpl::true_) - { - return f(context); - } - - template - 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 rng(first, last); - auto val_context = make_context(rcontext, context); - auto where_context = make_context(rng, val_context); - auto attr_context = make_context(attr, where_context); - return detail::call(f, attr_context, is_callable()); - } -}}} +} // boost::spirit::x3 #endif diff --git a/test/x3/rule3.cpp b/test/x3/rule3.cpp index 9e3a577c5..1d42a480c 100644 --- a/test/x3/rule3.cpp +++ b/test/x3/rule3.cpp @@ -13,6 +13,7 @@ #include #include + #include #include #include @@ -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 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 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{} = eps[([] (auto& ctx) { using boost::spirit::x3::_val; - static_assert(std::is_same, unused_type>::value, - "Attribute must not be synthesized"); + static_assert( + std::is_same_v, 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);