From a9ce7c200fa0f69223a54e8933ff3cbe34d7aa21 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:26:39 +0900 Subject: [PATCH] Canonicalize the attribute type of `a | a` --- include/iris/x4/operator/alternative.hpp | 72 +++++++++++++++++----- test/x4/alternative.cpp | 78 +++++++++++++++++++++++- 2 files changed, 133 insertions(+), 17 deletions(-) diff --git a/include/iris/x4/operator/alternative.hpp b/include/iris/x4/operator/alternative.hpp index 91ff59d5e..aefe652b3 100644 --- a/include/iris/x4/operator/alternative.hpp +++ b/include/iris/x4/operator/alternative.hpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -31,8 +32,49 @@ namespace iris::x4 { template struct alternative : binary_parser> { - using attribute_type = traits::attribute_of_binary::type; - +private: + static constexpr bool is_both_same_attribute = std::is_same_v< + typename parser_traits::attribute_type, + typename parser_traits::attribute_type + >; + +public: + // Canonicalize `rvariant` to `X` + using attribute_type = std::conditional_t< + is_both_same_attribute, + std::type_identity::attribute_type>, + traits::attribute_of_binary + >::type; + + // If canonicalized, proxy the underlying `sequence_size`. In other words: + // + // Let `X` be some tuple `tuple`, and let `Y` be some tuple `tuple`. + // Let `XP` be the parser that yields the attribute type `X`. Define `YP` likewise. + // + // `alternative` yields attribute type `rvariant`, and has `sequence_size` of 1. + // `alternative` yields attribute type `X`, and has `sequence_size` of 3. + // + // Without canonicalization, the latter case would have attribute type `rvariant` + // and `sequence_size` of 1. This will break automatic decomposition of tuple-like attribute. + // See `attribute.cpp` for example. + // + static constexpr std::size_t sequence_size = + is_both_same_attribute && parser_traits::sequence_size == parser_traits::sequence_size ? + parser_traits::sequence_size : // proxy value + static_cast(!X4UnusedAttribute); // unused = 0, otherwise 1 + +private: + template + using temp_attr_t = std::conditional_t< + is_both_same_attribute, + // Always the plain (non-lvalue) type + attribute_type, + // in case of tuple-like attribute, this might be a decomposed tuple that contains + // lvalue reference(s), which cannot be default-constructed + Attr + >; + +public: using binary_parser::binary_parser; template Se, class Context, X4UnusedAttribute UnusedAttr> @@ -65,25 +107,25 @@ struct alternative : binary_parser> noexcept( noexcept(detail::parse_alternative(this->left, first, last, ctx, attr)) && noexcept(detail::parse_alternative(this->right, first, last, ctx, attr)) && - std::is_nothrow_default_constructible_v && - noexcept(x4::move_to(std::declval(), attr)) + std::is_nothrow_default_constructible_v> && + noexcept(x4::move_to(std::declval>(), attr)) ) { static_assert( - std::default_initializable, + std::default_initializable>, "Attribute needs to be default-initializable to support rollback on failed parse attempt." ); - if (Attr attr_temp; detail::parse_alternative(this->left, first, last, ctx, attr_temp)) { - x4::move_to(std::move(attr_temp), attr); + if (temp_attr_t temp_attr; detail::parse_alternative(this->left, first, last, ctx, temp_attr)) { + x4::move_to(std::move(temp_attr), attr); return true; } if constexpr (has_context_v) { if (x4::has_expectation_failure(ctx)) return false; } - if (Attr attr_temp; detail::parse_alternative(this->right, first, last, ctx, attr_temp)) { - x4::move_to(std::move(attr_temp), attr); + if (temp_attr_t temp_attr; detail::parse_alternative(this->right, first, last, ctx, temp_attr)) { + x4::move_to(std::move(temp_attr), attr); return true; } return false; // `attr` is untouched @@ -132,20 +174,20 @@ struct alternative : binary_parser> // Non-empty container // Since the attribute is a container, we can reuse the buffer when the `left` fails // - unwrap_container_appender_t attr_temp; + unwrap_container_appender_t temp_attr; - if (detail::parse_alternative(this->left, first, last, ctx, attr_temp)) { - x4::move_to(std::move(attr_temp), attr); + if (detail::parse_alternative(this->left, first, last, ctx, temp_attr)) { + x4::move_to(std::move(temp_attr), attr); return true; } if constexpr (has_context_v) { if (x4::has_expectation_failure(ctx)) return false; } - traits::clear(attr_temp); // Reuse the buffer + traits::clear(temp_attr); // Reuse the buffer - if (detail::parse_alternative(this->right, first, last, ctx, attr_temp)) { - x4::move_to(std::move(attr_temp), attr); + if (detail::parse_alternative(this->right, first, last, ctx, temp_attr)) { + x4::move_to(std::move(temp_attr), attr); return true; } return false; // `attr` is untouched diff --git a/test/x4/alternative.cpp b/test/x4/alternative.cpp index bd432b9f7..5b4adf757 100644 --- a/test/x4/alternative.cpp +++ b/test/x4/alternative.cpp @@ -32,6 +32,8 @@ #include #include +#include +#include struct di_ignore { @@ -227,7 +229,7 @@ TEST_CASE("alternative") constexpr auto parser = +true_; using Parser = std::remove_const_t; using Attr = iris::rvariant>; - + using attribute_type = x4::parser_traits::attribute_type; STATIC_CHECK(std::same_as>); @@ -241,7 +243,7 @@ TEST_CASE("alternative") constexpr auto parser = +char_; using Parser = std::remove_const_t; using Attr = iris::rvariant; - + using attribute_type = x4::parser_traits::attribute_type; STATIC_CHECK(std::same_as); @@ -338,3 +340,75 @@ TEST_CASE("alternative") CHECK(parse("abaabb", +('a' >> attr(Foo{}) | 'b' >> attr(int{})), x)); } } + +TEST_CASE("alternative of same attributes (a | a)") +{ + using x4::int_; + using x4::bool_; + + // The subject is `int_ >> bool_` (non-container alternative) + { + constexpr auto int_bool = int_ >> bool_ | int_ >> bool_; + + using Parser = std::remove_const_t; + STATIC_CHECK(std::same_as< + x4::parser_traits::attribute_type, + alloy::tuple + >); + STATIC_CHECK(x4::parser_traits::sequence_size == 2); + + alloy::tuple var; + auto const res = parse("42true", int_bool, var); + REQUIRE(res.completed()); + CHECK(var == decltype(var){42, true}); + } + { + constexpr auto int_bool = int_ >> bool_ | int_ >> bool_; + constexpr auto foo_int_bool = x4::string("foo") >> int_bool; + + using Parser = std::remove_const_t; + STATIC_CHECK(std::same_as< + x4::parser_traits::attribute_type, + alloy::tuple + >); + STATIC_CHECK(x4::parser_traits::sequence_size == 3); + + alloy::tuple var; + auto const res = parse("foo42true", foo_int_bool, var); + REQUIRE(res.completed()); + CHECK(var == decltype(var){"foo", 42, true}); + } + + // The subject is `+bool_` (container alternative) + { + constexpr auto bools = +bool_ | +bool_; + + using Parser = std::remove_const_t; + STATIC_CHECK(std::same_as< + x4::parser_traits::attribute_type, + std::vector + >); + STATIC_CHECK(x4::parser_traits::sequence_size == 1); + + std::vector var; + auto const res = parse("truefalse", bools, var); + REQUIRE(res.completed()); + CHECK(var == decltype(var){true, false}); + } + { + constexpr auto bools = +bool_ | +bool_; + constexpr auto foo_bools = x4::string("foo") >> bools; + + using Parser = std::remove_const_t; + STATIC_CHECK(std::same_as< + x4::parser_traits::attribute_type, + alloy::tuple> + >); + STATIC_CHECK(x4::parser_traits::sequence_size == 2); + + alloy::tuple> var; + auto const res = parse("footruefalse", foo_bools, var); + REQUIRE(res.completed()); + CHECK(var == decltype(var){"foo", {true, false}}); + } +}