diff --git a/CMakeLists.txt b/CMakeLists.txt index 663704931..1a6ec49ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,10 @@ if(NOT TARGET Iris::Iris) add_subdirectory("${_iris_src}" "${CMAKE_CURRENT_BINARY_DIR}/iris") endif() +if(PROJECT_IS_TOP_LEVEL) + iris_set_msvc_debug_info() +endif() + # ----------------------------------------------------------------- # Create the main X4 target diff --git a/README.md b/README.md index bdcd55187..a15e695ab 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,19 @@ The signature of a semantic action is `[](auto&& ctx) { /* ... */ }`. > > The primary intended use of semantic actions is to handle cases that require ad hoc transformation, such as constructing a binary operator object through a more complex algorithm like precedence climbing. +#### Accessing the attribute of a multi-branch alternative + +Normally, `x4::_attr(ctx)` returns a reference to the attribute of the parser the action is attached to. However, when the action is attached to an alternative with a branch that has no attribute, a match may produce no attribute at all. In such cases, the action should access the attribute through `x4::visit_attr`: + +```cpp +constexpr auto timeout = ((x4::int_ >> "ms") | x4::lit("auto")).on_match([](auto&& ctx) { + x4::visit_attr( + ctx, + [](int& ms) { ms = std::min(ms, 1000); }, + [](x4::unused_type) {} // corresponds to the "auto" branch + ); +}); +``` ## Directory Structure @@ -200,7 +213,7 @@ These facilities are used less frequently in ordinary language grammars. | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `as(p)` | Force `p` to expose `T` as its attribute type. When the outer destination has another compatible type, parse into a temporary `T` and move the result into the destination. | | `fixed_value(value)` | Always succeed without consuming input and copy the stored `value` into the exposed attribute. | -| `reset_value` | Always succeed without consuming input and reset the exposed attribute. Containers are cleared; other values are assigned a value-initialized instance. | +| `default_value` | Always succeed without consuming input, construct `T{}` on each parse and write it into the exposed attribute.
Unlike `fixed_value(T{})`, which copies its stored value, it stores no `T`, so the parser can be a `constexpr` variable even when a value of `T` cannot be one (e.g. a type whose default value owns dynamically allocated storage). | | `unique_ptr(p)` | Expose a `std::unique_ptr` attribute and parse `p` into its pointee. Deduce the pointee type from the attribute of `p`. | | `unique_ptr(p)` | Expose a `std::unique_ptr` attribute and parse `p` into its pointee. An optional deleter type `D` may also be specified. | | `shared_ptr(p)` | Expose a `std::shared_ptr` attribute and parse `p` into its pointee. Deduce the pointee type from the attribute of `p`. | @@ -291,7 +304,17 @@ constexpr auto p = x4::int_.on_match([](auto&& ctx) { | Syntax | Meaning | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `_attr(ctx)` | Return the reference to the attribute associated with the current semantic action. This is the attribute instance produced by the parser `p` on the expression `p.on_match(f)`. It is available only when that parser exposes a non-`unused_type` attribute. Equivalent to `x4::get(ctx)`. | +| `_attr(ctx)` | Return the reference to the attribute associated with the current semantic action. This is the attribute instance produced by the parser `p` on the expression `p.on_match(f)`. It is available only when that parser exposes a non-`unused_type` attribute. Equivalent to `x4::get(ctx)`.
*Note:* when `p` is an alternative with a branch that has no attribute, use [`x4::visit_attr`](#accessing-the-attribute-of-a-multi-branch-alternative) instead. | | `_rule_var(ctx)` | Return the reference to the attribute variable of the innermost active `x4::rule` invocation. In a recursive rule, this always refers to the current recursive invocation rather than an outer invocation. Equivalent to `x4::get(ctx)`. | | `_local_var(ctx)` | Return the reference to the innermost local variable created by `with_local[p]` using the default `x4::contexts::local_var` context id. Equivalent to `x4::get(ctx)`.
*Note:* when `with_local[p]` uses a custom id, retrieve the value with `x4::get(ctx)` instead. | | `_as_var(ctx)` | Return the reference to the attribute variable managed by the innermost active `as(p)` facility. Semantic actions inside `p` can use this fetcher to access the value being constructed for the enclosing `as` parser. Equivalent to `x4::get(ctx)`. | + +## Developer's Notes + +### Naming Conventions + +#### Concepts + +- **Public contracts:** `x4::X4PascalCase` (e.g. `X4Attribute`, `X4StrictlyWritable`). A requirement X4 imposes on the user and checks at its public API. +- **Internal traits:** `x4::detail::snake_case`. A building block of a specific algorithm or logic inside X4, not intended for the user. +- **Public building blocks for parser authors:** `x4::snake_case`. A helper with which a parser declares its own properties; useful for user-defined parsers as well. diff --git a/cpp.hint b/cpp.hint new file mode 100644 index 000000000..1579c421b --- /dev/null +++ b/cpp.hint @@ -0,0 +1,18 @@ +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 + +#define IRIS_ALLOY_ADAPT_STRUCT + +#define IRIS_X4_DECLARE +#define IRIS_X4_DECLARE_CONSTEXPR +#define IRIS_X4_DEFINE +#define IRIS_X4_DEFINE_CONSTEXPR +#define IRIS_X4_INSTANTIATE diff --git a/include/iris/x4.hpp b/include/iris/x4.hpp index 55dd2fd0d..870dccd09 100644 --- a/include/iris/x4.hpp +++ b/include/iris/x4.hpp @@ -17,12 +17,14 @@ #include #include #include -#include #include #include #include +#include #include #include +#include + #endif diff --git a/include/iris/x4/attribute/as.hpp b/include/iris/x4/attribute/as.hpp index 75e4a34ed..0329f95a0 100644 --- a/include/iris/x4/attribute/as.hpp +++ b/include/iris/x4/attribute/as.hpp @@ -10,8 +10,11 @@ =============================================================================*/ #include -#include #include +#include +#include +#include +#include #include #include @@ -59,9 +62,9 @@ struct as_type_parser : unary_parser, Subject> static constexpr bool has_action = false; // Explicitly re-enable attribute detection in `x4::rule` static constexpr bool requires_exact_attribute_type = true; - // `as_type_parser` should NOT inherit underlying parser's `handles_container` + // `as_type_parser` should NOT inherit underlying parser's `accepts_container` // because `as_type_parser` is an atomic parser. The default implementation of - // `parser_traits>::handles_container` must transparently + // `parser_traits>::accepts_container` must transparently // handle this case. using unary_parser::unary_parser; @@ -73,17 +76,24 @@ struct as_type_parser : unary_parser, Subject> >; public: - // `outer_parser(as(subject))` forwards the outer `T&` (exposed attribute) for the subject + // `outer_parser(as(subject))` forwards the outer `T&` (exposed attribute) for the subject, unless + // it is a container which already holds the preceding results template Se, class Context, X4Attribute OuterAttr> requires std::same_as, T> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, OuterAttr& outer_attr) const { - if constexpr (Subject::has_action) { - return this->subject.parse(first, last, x4::replace_first_context(ctx, outer_attr), unused); - } else { - return this->subject.parse(first, last, ctx, outer_attr); + if constexpr (traits::X4Container) { + if (!std::ranges::empty(outer_attr)) { + // The container holds the preceding results, which the attribute of `as` + // is kept apart from: parse into a new attribute and append it on success + T attr_{}; + if (!this->parse_subject(first, last, ctx, attr_)) return false; + planner::pass_declared_attribute(outer_attr, std::move(attr_)); + return true; + } } + return this->parse_subject(first, last, ctx, outer_attr); } // `outer_parser(as(subject))` forwards `unused` for the subject @@ -100,30 +110,32 @@ struct as_type_parser : unary_parser, Subject> } } - // `outer_parser(as(subject))` forwards temporary `T` local variable for the subject, then move the variable to `U&` + // `outer_parser(as(subject))` parses into the element of `U` if `U` holds exactly `T` as its single + // element; otherwise into a temporary `T`, then passes it to `U&` by the ordinary conversion and assignment, + // never reinterpreting it into another structure (the assigned value is appended to a container which holds + // the preceding results) template Se, class Context, X4NonUnusedAttribute OuterAttr> requires (!std::same_as, T>) [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, OuterAttr& outer_attr) const { - // Ideally we should default to default-initialization and avoid value-initialization. - // However, there is currently no way to determine whether the attribute is ever touched - // by the underlying parser (for example: semantic action). - // - // Note that this behavior is our implementation details. The underlying parser should - // not rely on this behavior; they should never assume the given attribute is defaulted - // to some arbitrary initial value. - T attr_{}; // value-initialize + if constexpr (!has_attribute) { + return this->parse(first, last, ctx, unused); // equivalent to `omit[subject]` + + } else if constexpr (detail::holds_as_single_element, T>) { + return this->parse(first, last, ctx, alloy::get<0>(outer_attr)); - if constexpr (Subject::has_action) { - if (!this->subject.parse(first, last, x4::replace_first_context(ctx, attr_), unused)) return false; } else { - if (!this->subject.parse(first, last, ctx, attr_)) return false; - } + static_assert(X4StrictlyWritable&, unwrap_recursive_t&&>); + static_assert(!detail::dangles, unwrap_recursive_t&&>); + + T attr_{}; // value-initialize - x4::move_to(std::move(attr_), outer_attr); - return true; + if (!this->parse_subject(first, last, ctx, attr_)) return false; + planner::pass_declared_attribute(outer_attr, iris::unwrap_recursive(std::move(attr_))); + return true; + } } [[nodiscard]] /*constexpr*/ std::string get_x4_info() const @@ -131,6 +143,19 @@ struct as_type_parser : unary_parser, Subject> return std::string("as<") + typeid(T).name() + ">(" + get_info{}(this->subject) + ')'; } + +private: + // Parses the subject into `attr`, the attribute of `as`; an action in the subject refers to it as `_as_var` + template Se, class Context, X4Attribute Attr> + [[nodiscard]] constexpr bool + parse_subject(It& first, Se const& last, Context const& ctx, Attr& attr) const + { + if constexpr (Subject::has_action) { + return this->subject.parse(first, last, x4::replace_first_context(ctx, attr), unused); + } else { + return this->subject.parse(first, last, ctx, attr); + } + } }; namespace detail { diff --git a/include/iris/x4/attribute/smart_ptr.hpp b/include/iris/x4/attribute/smart_ptr.hpp index 54808c722..8b1de0582 100644 --- a/include/iris/x4/attribute/smart_ptr.hpp +++ b/include/iris/x4/attribute/smart_ptr.hpp @@ -151,6 +151,9 @@ struct unique_ptr_parser : detail::unique_ptr_parser_base< static constexpr bool requires_exact_attribute_type = true; + template + static constexpr bool accepts_container = false; + using unique_ptr_parser::unique_ptr_parser_base::unique_ptr_parser_base; [[nodiscard]] constexpr std::string get_x4_info() const @@ -319,6 +322,9 @@ struct shared_ptr_parser : detail::shared_ptr_parser_base< static constexpr bool requires_exact_attribute_type = true; + template + static constexpr bool accepts_container = false; + using shared_ptr_parser::shared_ptr_parser_base::shared_ptr_parser_base; [[nodiscard]] constexpr std::string get_x4_info() const diff --git a/include/iris/x4/attribute/value.hpp b/include/iris/x4/attribute/value.hpp index e5a0b07c3..6be99f165 100644 --- a/include/iris/x4/attribute/value.hpp +++ b/include/iris/x4/attribute/value.hpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include @@ -65,10 +65,9 @@ struct fixed_value_parser : parser> template Se, class Context, X4Attribute Attr> [[nodiscard]] constexpr bool parse(It&, Se const&, Context const&, Attr& exposed_attr) const - noexcept(noexcept(x4::move_to(std::declval(), exposed_attr))) { // Always copy (need reuse in repetitive invocations) - x4::move_to(this->held_value_, exposed_attr); + x4::write_attribute(exposed_attr, this->held_value_); return true; } @@ -76,7 +75,7 @@ struct fixed_value_parser : parser> IRIS_NO_UNIQUE_ADDRESS HeldValueT held_value_{}; }; -// aka `reset_value` +// aka `default_value` template struct fixed_value_parser : parser> { @@ -92,22 +91,13 @@ struct fixed_value_parser : parser> return true; } - template Se, class Context, X4NonUnusedAttribute ContainerAttr> - requires CategorizedAttr - [[nodiscard]] static constexpr bool - parse(It&, Se const&, Context const&, ContainerAttr& exposed_attr) noexcept - { - traits::clear(exposed_attr); - return true; - } - + // `T{}` by the ordinary write rules: an empty `std::vector` adds no element to a `std::vector`, + // but one to a `std::vector>` template Se, class Context, X4NonUnusedAttribute Attr> - requires (!CategorizedAttr) [[nodiscard]] static constexpr bool parse(It&, Se const&, Context const&, Attr& exposed_attr) - noexcept(noexcept(exposed_attr = Attr{})) { - exposed_attr = Attr{}; + x4::write_attribute(exposed_attr, T{}); return true; } }; @@ -136,7 +126,7 @@ struct get_info> operator()(fixed_value_parser const&) const { if constexpr (std::is_void_v) { - return "reset_value"; + return "default_value"; } else { return "fixed_value(...)"; } @@ -186,19 +176,17 @@ namespace parsers { // to the given parameter. Copies the held instance on each invocation. [[maybe_unused]] inline constexpr detail::fixed_value_gen fixed_value{}; -// A special `fixed_value` parser that resets the variable and always succeeds. -// -// This can be used for constructing `constexpr` instance of a parser -// even when `T` has dynamically allocated storage. -// For example, normal `fixed_value(std::vector{})` cannot be assigned -// to a `constexpr` instance, but `reset_value>` can. +// An always-succeeding parser which constructs `T{}` on each invocation and writes it by the ordinary +// write rules. Unlike `fixed_value(T{})` it holds no `T`, so it can be a `constexpr` variable even where +// a `T` cannot: given `struct Defaults { std::vector values{1, 2, 3}; };`, +// `constexpr auto p = default_value;` is well-formed, `fixed_value(Defaults{})` is not. template -[[maybe_unused]] inline constexpr fixed_value_parser reset_value{}; +[[maybe_unused]] inline constexpr fixed_value_parser default_value{}; } // parsers using parsers::fixed_value; -using parsers::reset_value; +using parsers::default_value; } // iris::x4 diff --git a/include/iris/x4/char/char_class.hpp b/include/iris/x4/char/char_class.hpp index 8740300e6..4208eeb50 100644 --- a/include/iris/x4/char/char_class.hpp +++ b/include/iris/x4/char/char_class.hpp @@ -131,21 +131,6 @@ IRIS_X4_CHAR_CLASSES(standard_wide) #undef IRIS_X4_CHAR_CLASS #undef IRIS_X4_CHAR_CLASSES -// Don't put these in namespace `parsers`, these are too much - -using x4::standard::alnum; -using x4::standard::alpha; -using x4::standard::digit; -using x4::standard::xdigit; -using x4::standard::cntrl; -using x4::standard::graph; -using x4::standard::lower; -using x4::standard::print; -using x4::standard::punct; -using x4::standard::space; -using x4::standard::blank; -using x4::standard::upper; - namespace detail { diff --git a/include/iris/x4/char/char_parser.hpp b/include/iris/x4/char/char_parser.hpp index c07e8b0a8..be06f7608 100644 --- a/include/iris/x4/char/char_parser.hpp +++ b/include/iris/x4/char/char_parser.hpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include @@ -81,14 +81,6 @@ struct char_parser : parser> requires has_static_test [[nodiscard]] static constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) - noexcept( - std::is_nothrow_copy_assignable_v && - noexcept(x4::skip_over(first, last, ctx)) && - noexcept(first != last) && - noexcept(Derived::test(static_cast(*first), ctx)) && - noexcept(x4::move_to(std::iter_value_t{*first}, attr)) && - noexcept(++first) - ) { static_assert(!CharIncompatibleWith, char_type>, "Mixing incompatible char types is not allowed"); static_assert(!CharLike || !CharIncompatibleWith, "Mixing incompatible char types is not allowed"); @@ -97,7 +89,10 @@ struct char_parser : parser> x4::skip_over(it, last, ctx); if (it != last && Derived::test(static_cast(*it), ctx)) { - x4::move_to(std::iter_value_t{*it++}, attr); + if constexpr (x4::has_attribute_v) { + x4::write_attribute(attr, std::iter_value_t{*it}); + } + ++it; first = it; return true; } @@ -108,14 +103,6 @@ struct char_parser : parser> requires (!has_static_test) [[nodiscard]] constexpr bool parse(this Self const& self /* require const& */, It& first, Se const& last, Context const& ctx, Attr& attr) - noexcept( - std::is_nothrow_copy_assignable_v && - noexcept(x4::skip_over(first, last, ctx)) && - noexcept(first != last) && - noexcept(self.test(static_cast(*first), ctx)) && - noexcept(x4::move_to(std::iter_value_t{*first}, attr)) && - noexcept(++first) - ) { static_assert(!CharIncompatibleWith, char_type>, "Mixing incompatible char types is not allowed"); static_assert(!CharLike || !CharIncompatibleWith, "Mixing incompatible char types is not allowed"); @@ -124,7 +111,10 @@ struct char_parser : parser> x4::skip_over(it, last, ctx); if (it != last && self.test(static_cast(*it), ctx)) { - x4::move_to(std::iter_value_t{*it++}, attr); + if constexpr (x4::has_attribute_v) { + x4::write_attribute(attr, std::iter_value_t{*it}); + } + ++it; first = it; return true; } diff --git a/include/iris/x4/core/action.hpp b/include/iris/x4/core/action.hpp index 444c4648a..4d1fbdc6b 100644 --- a/include/iris/x4/core/action.hpp +++ b/include/iris/x4/core/action.hpp @@ -12,15 +12,27 @@ #include // IWYU pragma: keep +#include +#include + +#include + #include #include #include +#include #include +#include +#include + +#include +#include #include #include #include +#include #include #include @@ -28,6 +40,83 @@ namespace iris::x4 { namespace detail { +struct action_attribute_access; + +// Returned by `x4::_attr` when the subject of the action may succeed without writing the attribute. +// The semantic action can read it only through `x4::visit_attr`. +// +// The visitor is called with: +// - `unused` if the match wrote nothing +// - the candidate held if `IsMultiCand` (the attribute is a variant of the candidates of an alternative) +// - otherwise the attribute itself +template +class action_attribute_view +{ + constexpr explicit action_attribute_view(A* attr) noexcept + : attr_(attr) + {} + + A* attr_; // null if no attribute + + friend struct action_attribute_access; +}; + +template +struct is_action_attribute_view : std::false_type {}; + +template +struct is_action_attribute_view> : std::true_type {}; + +template +inline constexpr bool is_action_attribute_view_v = is_action_attribute_view>::value; + +struct action_attribute_access +{ + template + [[nodiscard]] static constexpr action_attribute_view make(A* attr) noexcept + { + return action_attribute_view(attr); + } + + template + static constexpr void visit(action_attribute_view const& view, Visitor&& visitor) + { + if (!view.attr_) { + std::forward(visitor)(unused); + return; + } + if constexpr (IsMultiCand) { + view.attr_->visit([&](auto& candidate) { std::forward(visitor)(iris::unwrap_recursive(candidate)); }); + + } else { + std::forward(visitor)(iris::unwrap_recursive(*view.attr_)); + } + } +}; + +} // detail + +// Calls `fs` with the attribute the match of a semantic action wrote, as `T&`, or with `unused_type` +// if it wrote none. Two or more functions are combined by `iris::overloaded`, which copies or moves them. +template +constexpr void visit_attr(Context const& ctx, Fs&&... fs) +{ + static_assert(sizeof...(Fs) >= 1); + auto const& view = x4::_attr(ctx); + static_assert( + detail::is_action_attribute_view_v, + "`x4::visit_attr` takes the attribute of a subject which may succeed without writing it" + ); + if constexpr (sizeof...(Fs) == 1) { + detail::action_attribute_access::visit(view, std::forward(fs)...); + + } else { + detail::action_attribute_access::visit(view, iris::overloaded{std::forward(fs)...}); + } +} + +namespace detail { + template struct action_context; @@ -73,14 +162,17 @@ struct action : proxy_parser, Subject> static constexpr bool need_rcontext = true; static constexpr bool requires_exact_attribute_type = false; // reset - ActionF f; + // The subject may succeed without writing its attribute (an alternative with a branch without + // an attribute): the action sees the attribute the match wrote, or none (see `x4::visit_attr`) + static constexpr bool sees_written_attribute = + has_attribute_v && detail::may_leave_attribute_unwritten_v; 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)) + , f_(std::forward(f)) { } @@ -88,11 +180,19 @@ struct action : proxy_parser, Subject> // Since we can assume that the subject unconditionally requires `_attr`, // we must create a temporary variable to pass it to the subject. template Se, class Context, X4UnusedAttribute UnusedAttr> + requires (!sees_written_attribute) [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, UnusedAttr&) const { + It local_it = first; typename base_type::attribute_type attr_temp{}; // value-initialize - return this->parse_main(first, last, ctx, attr_temp); + if (!this->subject.parse(local_it, last, ctx, attr_temp)) return false; + + if (this->call_action(ctx, attr_temp)) { + first = local_it; + return true; + } + return false; } private: @@ -117,21 +217,61 @@ struct action : proxy_parser, Subject> public: // When the exposed attribute is NOT `unused_type`. template Se, class Context, X4NonUnusedAttribute Attr> - requires can_pass_exposed_attr + requires (!sees_written_attribute) && can_pass_exposed_attr [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const { - return this->parse_main(first, last, ctx, attr); + It local_it = first; + if (!this->subject.parse(local_it, last, ctx, attr)) return false; + + if (this->call_action(ctx, attr)) { + first = local_it; + return true; + } + return false; } // When the exposed attribute is NOT `unused_type`. template Se, class Context, X4NonUnusedAttribute Attr> - requires (!can_pass_exposed_attr) + requires (!sees_written_attribute) && (!can_pass_exposed_attr) [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& /* attr is discarded */) const { typename base_type::attribute_type attr_temp{}; // value-initialize - return this->parse_main(first, last, ctx, attr_temp); + It local_it = first; + if (!this->subject.parse(local_it, last, ctx, attr_temp)) return false; + + if (this->call_action(ctx, attr_temp)) { + first = local_it; + return true; + } + return false; + } + + // The subject may succeed without writing its attribute. The subject parses into a slot of + // the action, which records whether the match wrote the attribute; the action sees it through + // a view, and the attribute is written into `attr` only when the action accepts the match. + template Se, class Context, X4Attribute Attr> + requires sees_written_attribute + [[nodiscard]] constexpr bool + parse(It& first, Se const& last, Context const& ctx, Attr& attr) const + { + using attribute_type = base_type::attribute_type; + constexpr bool is_multi_cand = detail::attribute_candidates_t::size >= 2; + + It const saved_first = first; + detail::action_slot slot; + if (!this->subject.parse(first, last, ctx, slot)) return false; + + auto const view = detail::action_attribute_access::make( + slot.is_generated() ? std::addressof(slot.value()) : nullptr + ); + if (!this->call_action(ctx, view)) { + first = saved_first; + return false; + } + action::commit(attr, slot); + return true; } constexpr void operator[](auto const&) const = delete; // You can't add semantic action for semantic action @@ -142,6 +282,39 @@ struct action : proxy_parser, Subject> } private: + template + static constexpr void commit(Attr& attr, detail::action_slot& slot) + { + if constexpr (X4UnusedAttribute) { + return; + + } else if constexpr (detail::is_action_slot_v) { + if (!slot.is_generated()) { + attr.disengage(); + return; + } + auto& engaged = attr.engage(); + detail::write_slot_value(slot, [&engaged](V&& value) { + x4::write_attribute(engaged, std::forward(value)); + }); + + } else if constexpr (traits::X4Container>) { + if (slot.is_generated()) { + detail::write_slot_value(slot, [&attr](V&& value) { + x4::write_attribute(attr, std::forward(value)); + }); + } + + } else { + traits::attribute_traits::reset(attr); + if (slot.is_generated()) { + detail::write_slot_value(slot, [&attr](V&& value) { + x4::write_attribute(attr, std::forward(value)); + }); + } + } + } + // Semantic action with no parameter: `p[([] { /* ... */ })]` template [[nodiscard]] constexpr bool @@ -164,9 +337,9 @@ struct action : proxy_parser, Subject> ); if constexpr (action_returns_bool) { - return this->f(); + return this->f_(); } else { - this->f(); + this->f_(); return true; } } @@ -187,17 +360,17 @@ struct action : proxy_parser, Subject> // Inject `_attr` only when `Attr` is not `unused_type` if constexpr (X4UnusedAttribute) { if constexpr (action_returns_bool) { - return this->f(ctx); + return this->f_(ctx); } else { - this->f(ctx); + this->f_(ctx); return true; } } else { if constexpr (action_returns_bool) { - return this->f(x4::make_context(attr, ctx)); + return this->f_(x4::make_context(attr, ctx)); } else { - this->f(x4::make_context(attr, ctx)); + this->f_(x4::make_context(attr, ctx)); return true; } } @@ -217,21 +390,8 @@ struct action : proxy_parser, Subject> return false; // dummy } - template Se, class Context, X4Attribute Attr> - [[nodiscard]] constexpr bool - parse_main(It& first, Se const& last, Context const& ctx, Attr& attr) const - { - It const saved_first = first; - if (!this->subject.parse(first, last, ctx, attr)) return false; - - if (this->call_action(ctx, attr)) { - return true; - } - // reset iterators if semantic action failed the match - // retrospectively - first = saved_first; - return false; - } +private: + ActionF f_; }; } // iris::x4 diff --git a/include/iris/x4/core/attribute.hpp b/include/iris/x4/core/attribute.hpp index 4532639a2..caffa5988 100644 --- a/include/iris/x4/core/attribute.hpp +++ b/include/iris/x4/core/attribute.hpp @@ -15,6 +15,7 @@ #include #include +#include // IWYU pragma: keep #include namespace iris::alloy { @@ -59,6 +60,26 @@ concept X4NonUnusedAttribute = template concept X4Attribute = X4UnusedAttribute || X4NonUnusedAttribute; +namespace detail { + +template +inline constexpr bool mandates_writable_ref = [] { + static_assert(std::is_lvalue_reference_v); + static_assert(!std::is_const_v>); + return true; +}(); + +template +concept dangles = std::ranges::borrowed_range && !std::ranges::borrowed_range; + +} // detail + +template +concept X4StrictlyWritable = + detail::mandates_writable_ref && + weakly_assignable_from && + is_convertible_without_narrowing_v>; + } // iris::x4 #endif diff --git a/include/iris/x4/core/container_appender.hpp b/include/iris/x4/core/container_appender.hpp deleted file mode 100644 index 3ea30861a..000000000 --- a/include/iris/x4/core/container_appender.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#ifndef IRIS_ZZ_X4_CORE_CONTAINER_APPENDER_HPP -#define IRIS_ZZ_X4_CORE_CONTAINER_APPENDER_HPP - -/*============================================================================= - Copyright (c) 2025 Nana Sakisaka - Copyright (c) 2026 The Iris Project Contributors - - 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) -=============================================================================*/ - -#include // IWYU pragma: keep - -#include -#include - -#include - -#include -#include -#include - -namespace iris::x4 { - -namespace detail { - -template -struct container_appender_base {}; - -template - requires traits::is_associative_v -struct container_appender_base -{ - using key_type = typename ContainerAttr::key_type; -}; - -} // detail - -template -struct container_appender : detail::container_appender_base -{ - static_assert(!is_ttp_specialization_of_v, container_appender>); - static_assert(traits::X4Container); - - using container_type = ContainerAttr; - using value_type = typename ContainerAttr::value_type; - - ContainerAttr& container; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) - - constexpr explicit container_appender(ContainerAttr& container) noexcept - : container(container) - {} - - constexpr auto begin() const - noexcept(noexcept(traits::begin(this->container))) - { - return traits::begin(this->container); - } - - constexpr auto begin() - noexcept(noexcept(traits::begin(this->container))) - { - return traits::begin(this->container); - } - - constexpr auto end() const - noexcept(noexcept(traits::end(this->container))) - { - return traits::end(this->container); - } - - constexpr auto end() - noexcept(noexcept(traits::end(this->container))) - { - return traits::end(this->container); - } - - [[nodiscard]] constexpr bool empty() const - noexcept(noexcept(traits::is_empty(this->container))) - { - return traits::is_empty(this->container); - } - - constexpr void clear() - noexcept(noexcept(traits::clear(this->container))) - { - traits::clear(this->container); - } - - template - requires requires(ContainerAttr& container) { - traits::push_back(container, std::declval()); - } - constexpr void push_back(T&& value) - noexcept(noexcept(traits::push_back(this->container, std::forward(value)))) - { - traits::push_back(this->container, std::forward(value)); - } -}; - -template -struct unwrap_container_appender -{ - static_assert(!std::is_reference_v); - static_assert(!std::is_const_v); - using type = T; -}; - -template -using unwrap_container_appender_t = typename unwrap_container_appender::type; - -template -struct unwrap_container_appender> -{ - using type = ContainerAttr; -}; - - -template -[[nodiscard]] constexpr Attr& -make_container_appender(Attr& other_attr) noexcept -{ - return other_attr; // NOLINT(bugprone-return-const-ref-from-parameter) -} - -template -constexpr void -make_container_appender(Attr const&&) = delete; // dangling - -template -[[nodiscard]] constexpr container_appender -make_container_appender(ContainerAttr& container_attr) noexcept -{ - return container_appender{container_attr}; -} - -template - requires is_ttp_specialization_of_v, container_appender> -[[nodiscard]] constexpr ContainerAttr& -make_container_appender(ContainerAttr& appender) noexcept -{ - return appender; -} - -template -struct transform_attribute> -{ - using type = Transformed&; - - [[nodiscard]] static constexpr Transformed& pre(container_appender& appender) - { - return appender.container; - } - - template - static constexpr void post(container_appender&, TransformedT&&) noexcept - { - } -}; - -namespace traits { - -template -struct append_container> -{ - template Se> - static constexpr void call(container_appender& appender, It first, Se last) - noexcept(noexcept(traits::append(appender.container, std::move(first), std::move(last)))) - { - traits::append(appender.container, std::move(first), std::move(last)); - } -}; - -} // traits - -} // iris::x4 - -#endif diff --git a/include/iris/x4/core/detail/action_slot.hpp b/include/iris/x4/core/detail/action_slot.hpp new file mode 100644 index 000000000..f091fb7fa --- /dev/null +++ b/include/iris/x4/core/detail/action_slot.hpp @@ -0,0 +1,81 @@ +#ifndef IRIS_ZZ_X4_CORE_DETAIL_ACTION_SLOT_HPP +#define IRIS_ZZ_X4_CORE_DETAIL_ACTION_SLOT_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +#include // IWYU pragma: keep + +#include +#include + +#include +#include +#include + +namespace iris::x4::detail { + +// The attribute of the subject of a semantic action which may succeed without writing it, and +// whether the match wrote it. +// +// The action owns the slot and passes it to the subject as its attribute; it goes through the +// parsers which pass the attribute through, down to the alternative whose selected branch decides +// whether the attribute is written. +template +class action_slot +{ +public: + using attribute_type = A; + + // A new attribute in its default state, into which the branch writes + [[nodiscard]] constexpr A& engage() + { + return attr_.emplace(); + } + + // No attribute, as a branch without an attribute succeeds (or a branch failed) + constexpr void disengage() noexcept + { + attr_.reset(); + } + + [[nodiscard]] constexpr bool is_generated() const noexcept + { + return attr_.has_value(); + } + + [[nodiscard]] constexpr A& value() noexcept + { + return *attr_; + } + +private: + std::optional attr_; +}; + +template +inline constexpr bool is_action_slot_v = is_ttp_specialization_of_v, action_slot>; + +// Calls `writer` with the value in `slot`, moved. +// - `IsMultiCand`: the value is a variant of the attributes of the branches of `a | b | ...`. +// `write` is called with the one it holds, i.e. the attribute of the branch that matched. +// - Otherwise: `writer` is called with the value itself. +// A variant declared by `rule` or `as` is the attribute of one branch, so it is passed whole. +template +constexpr void write_slot_value(action_slot& slot, WriterF const& writer) +{ + if constexpr (IsMultiCand) { + slot.value().visit([&writer](auto& candidate) { writer(std::move(iris::unwrap_recursive(candidate))); }); + + } else { + writer(std::move(slot.value())); + } +} + +} // iris::x4::detail + +#endif diff --git a/include/iris/x4/core/detail/parse_alternative.hpp b/include/iris/x4/core/detail/parse_alternative.hpp index 320b6b553..4946436a9 100644 --- a/include/iris/x4/core/detail/parse_alternative.hpp +++ b/include/iris/x4/core/detail/parse_alternative.hpp @@ -14,12 +14,11 @@ #include #include -#include -#include +#include #include #include -#include +#include #include #include #include @@ -40,163 +39,6 @@ struct alternative; namespace iris::x4::detail { -struct pass_variant_unused -{ - using type = unused_type; - - template - [[nodiscard]] static constexpr unused_type - call(T&) noexcept - { - return unused_type{}; - } -}; - -template -struct pass_variant_used -{ - using type = Attr&; - - [[nodiscard]] static constexpr Attr& - call(Attr& v) noexcept - { - return v; - } -}; - -template<> -struct pass_variant_used : pass_variant_unused {}; - -template -struct pass_parser_attribute -{ - using attribute_type = parser_traits::attribute_type; - using substitute_type = variant_find_holdable_type::type; - - using type = std::conditional_t< - std::same_as, - Attr&, - substitute_type - >; - - template - requires std::same_as> - [[nodiscard]] static constexpr Attr_& - call(Attr_& attr) noexcept - { - return attr; - } - - template - requires (!std::same_as>) - [[nodiscard]] static type - call(Attr_&) - noexcept(std::is_nothrow_default_constructible_v) - { - return type{}; - } -}; - -// Pass non-variant attributes as-is -template -struct pass_non_variant_attribute -{ - using type = Attr&; - - [[nodiscard]] constexpr static Attr& - call(Attr& attribute) noexcept - { - return attribute; - } -}; - -// Unwrap single-element tuple-likes -template - requires SingleElementTupleLike -struct pass_non_variant_attribute -{ - using attr_type = std::remove_reference_t< - alloy::tuple_element_t<0, Attr> - >; - using pass = pass_parser_attribute; - using type = pass::type; - - template - [[nodiscard]] static constexpr type - call(Attr_& attr) - noexcept(noexcept(pass::call(alloy::get<0>(attr)))) - { - return pass::call(alloy::get<0>(attr)); - } -}; - -template - requires (!is_variant_v) -struct pass_parser_attribute - : pass_non_variant_attribute -{}; - -template -struct pass_parser_attribute - : pass_variant_unused -{}; - -template -struct pass_variant_attribute - : std::conditional_t< - has_attribute_v, - pass_parser_attribute, - pass_variant_unused - > -{}; - -template -struct pass_variant_attribute, Attr> - : std::conditional_t< - has_attribute_v>, - pass_variant_used, - pass_variant_unused - > -{}; - -template Se, class Context, X4Attribute Attr> - requires std::is_lvalue_reference_v::type> -[[nodiscard]] constexpr bool -parse_alternative( - Parser const& p, It& first, Se const& last, - Context const& ctx, Attr& attr -) { - return p.parse(first, last, ctx, pass_variant_attribute::call(attr)); -} - -template Se, class Context, X4Attribute Attr> - requires (!std::is_lvalue_reference_v::type>) -[[nodiscard]] constexpr bool -parse_alternative( - Parser const& p, It& first, Se const& last, - Context const& ctx, Attr& attr -) { - auto&& actual_attr = pass_variant_attribute::call(attr); - if (!p.parse(first, last, ctx, actual_attr)) return false; - x4::move_to(std::move(actual_attr), attr); - return true; -} - -template -struct alternative_helper : proxy_parser, Subject> -{ - using proxy_parser::proxy_parser; - - template Se, class Context, X4Attribute Attr> - [[nodiscard]] constexpr bool - parse(It& first, Se const& last, Context const& ctx, Attr& attr) const - { - return detail::parse_alternative(this->subject, first, last, ctx, attr); - } -}; -template -alternative_helper(Subject const&) -> alternative_helper; - template [[nodiscard]] constexpr bool alternative_should_stop(Context const& ctx) noexcept { @@ -215,24 +57,66 @@ struct parse_alternative_all_impl template [[nodiscard]] static constexpr bool parse_branch(Try&& try_branch, ExposedAttr& exposed_attr) { - using branch_attr = parser_traits>::attribute_type; - if constexpr (X4UnusedAttribute || traits::detail::clearable_for) { - auto&& alt_attr = detail::prepare_attribute(exposed_attr); - return try_branch.template operator()(alt_attr); + // Don't declare alias templates for parser type or attribute type here; + // Visual Studio often hides the real type when it is wrapped in a local alias. + + if constexpr ( + may_leave_attribute_unwritten_v> || + X4UnusedAttribute>::attribute_type> || + traits::detail::clearable_for>::attribute_type> + ) { + auto&& attr = detail::prepare_attribute_for>(exposed_attr); + return try_branch.template operator()(attr); } else { static_assert( - requires(branch_attr&& value) { x4::move_to(std::move(value), exposed_attr); }, + requires(typename parser_traits>::attribute_type&& value) { + x4::write_attribute(exposed_attr, std::move(value)); + }, "The attribute of this branch cannot be converted into the attribute of the alternative." ); // The branch yields a whole value of an unrelated shape (e.g. a narrower variant); // parse it into a temporary and convert on success. - branch_attr temp{}; + typename parser_traits>::attribute_type temp{}; + if (!try_branch.template operator()(temp)) return false; - x4::move_to(std::move(temp), exposed_attr); + + // As in the other branches, the value is written into the default state. An earlier + // branch may have failed after writing, and `write_attribute` writes into an existing + // content: it appends to a container, and writes nothing for a disengaged optional. + traits::attribute_traits::reset(exposed_attr); + x4::write_attribute(exposed_attr, std::move(temp)); return true; } } + + // The slot of a semantic action records whether the branch which matched wrote the attribute. + template + [[nodiscard]] static constexpr bool parse_slot_branch(Try&& try_branch, Slot& slot) + { + using branch_parser = nary::parser_t; + using attribute_type = Slot::attribute_type; + + if constexpr (!has_attribute_v) { + slot.disengage(); + return try_branch.template operator()(unused); + + } else if constexpr (may_leave_attribute_unwritten_v) { + slot.disengage(); + return try_branch.template operator()(slot); + + } else { + attribute_type& attr = slot.engage(); + bool matched = false; + if constexpr (traits::X4Container) { + matched = try_branch.template operator()(attr); // into the new, empty container + } else { + matched = parse_alternative_all_impl::parse_branch(try_branch, attr); + } + if (!matched) slot.disengage(); + return matched; + } + } }; // Tries the branches in order; stops at the first match, or at an expectation @@ -240,19 +124,6 @@ struct parse_alternative_all_impl template struct parse_alternative_all { - // Tries the branches in order without touching any attribute; used where each - // branch writes on its own (appending into a container). - template - [[nodiscard]] static constexpr bool - call(std::index_sequence, Try&& try_branch, Context const& ctx) - { - bool matched = false; - (void)((((matched = try_branch.template operator()())) || detail::alternative_should_stop(ctx)) || ...); - return matched; - } - - // ------------------------------------------------------------------------- - template [[nodiscard]] static constexpr bool call(std::index_sequence, Try&& try_branch, Context const& ctx, UnusedAttr const& unused_attr) @@ -263,7 +134,7 @@ struct parse_alternative_all } template - requires (!traits::X4Container) + requires (!traits::X4Container) && (!is_action_slot_v) [[nodiscard]] static constexpr bool call(std::index_sequence, Try&& try_branch, Context const& ctx, ExposedAttr& exposed_attr) { @@ -273,6 +144,16 @@ struct parse_alternative_all return matched; } + template + requires is_action_slot_v + [[nodiscard]] static constexpr bool + call(std::index_sequence, Try&& try_branch, Context const& ctx, Slot& slot) + { + bool matched = false; + (void)((((matched = parse_alternative_all_impl::template parse_slot_branch(try_branch, slot))) || detail::alternative_should_stop(ctx)) || ...); + return matched; + } + template requires traits::X4Container [[nodiscard]] static constexpr bool @@ -291,32 +172,41 @@ struct parse_alternative_all // The only situation we can guarantee such condition is when the container // is empty; assuming that the "empty" state of any user-provided container // class is monostate. - if (traits::is_empty(container_attr)) { + if (std::ranges::empty(container_attr)) { auto parse_branch = [&]() -> bool { - if (try_branch.template operator()(container_attr)) return true; - traits::clear(container_attr); - return false; + if constexpr (!has_attribute_v>) { + return try_branch.template operator()(unused); + + } else { + if (try_branch.template operator()(container_attr)) return true; + iris::container::clear(container_attr); + return false; + } }; bool matched = false; (void)((((matched = parse_branch.template operator()())) || detail::alternative_should_stop(ctx)) || ...); return matched; } - // The container already holds elements: a failed branch must not touch - // them, and there is no general way to undo appends, so each branch parses - // into a buffer that is appended only on success. - unwrap_container_appender_t buffer; + // --------------------------------------------------------------- + // The container already holds elements + + // A failed branch must not touch the existing elements, and there is no general + // way to undo appends. So each branch parses into a buffer that is appended only + // on success. + ContainerAttr buffer; auto parse_branch = [&]() -> bool { - if (try_branch.template operator()(buffer)) { - traits::append( - container_attr, - std::make_move_iterator(traits::begin(buffer)), - std::make_move_iterator(traits::end(buffer)) - ); - return true; + if constexpr (!has_attribute_v>) { + return try_branch.template operator()(unused); + + } else { + if (try_branch.template operator()(buffer)) { + iris::container::append_range(container_attr, buffer | std::views::as_rvalue); + return true; + } + iris::container::clear(buffer); + return false; } - traits::clear(buffer); - return false; }; bool matched = false; (void)((((matched = parse_branch.template operator()())) || detail::alternative_should_stop(ctx)) || ...); @@ -336,16 +226,12 @@ struct parse_into_container_impl> It& first, Se const& last, Context const& ctx, ExposedAttr& exposed_attr ) { - static_assert(traits::is_container_v); + static_assert(traits::X4Container); return parse_alternative_all::call( std::index_sequence_for{}, [&](auto& container_attr) { - if constexpr (is_variant_v::type>) { - return detail::parse_into_container(alternative_helper{nary::get(parser.elems)}, first, last, ctx, container_attr); - } else { - return detail::parse_into_container(nary::get(parser.elems), first, last, ctx, container_attr); - } + return detail::parse_into_container(nary::get(parser.elems), first, last, ctx, container_attr); }, ctx, exposed_attr diff --git a/include/iris/x4/core/detail/parse_into_container.hpp b/include/iris/x4/core/detail/parse_into_container.hpp index d377af12d..749ea0693 100644 --- a/include/iris/x4/core/detail/parse_into_container.hpp +++ b/include/iris/x4/core/detail/parse_into_container.hpp @@ -13,14 +13,17 @@ #include // IWYU pragma: keep #include -#include #include #include #include #include +#include +#include #include +#include +#include #include #include @@ -35,59 +38,217 @@ struct optional; namespace iris::x4::detail { -template -struct parser_accepts_container +enum class container_parse_strategy : unsigned char { - static constexpr bool value = - parser_traits::template handles_container && - !requires (Container& c, typename parser_traits::attribute_type&& v) { - traits::push_back(c, std::move(v)); - }; + // The value of the parser cannot be written into the container in any way. + none, + + // The container itself is passed to the parser, which appends into it. + container_itself, + + // The value of the parser is written into the container as a "part", i.e., + // a new element, the parts of a sequence, or an appended range. + as_part, + + // The parser may succeed without writing its value. It is passed a slot, and the value + // written (if any) is written into the container as a part. + as_part_if_written, }; +template +inline constexpr bool candidates_write_as_part = false; + +template +inline constexpr bool candidates_write_as_part> = + (planner::node_write_strategy_of>>.is_writable && ...); + template - requires is_variant_v::attribute_type> -struct parser_accepts_container +inline constexpr container_parse_strategy container_parse_strategy_for = [] { + using planner::branch_kind; + using container_type = planner::storage_t; + using value_type = planner::model_value_t::attribute_type>; + + constexpr planner::node_write_strategy strategy = planner::node_write_strategy_of>; + + if constexpr (may_leave_attribute_unwritten_v) { + if constexpr (candidates_write_as_part>) { + return container_parse_strategy::as_part_if_written; + + } else if constexpr (parser_traits::template accepts_container) { + return container_parse_strategy::container_itself; + + } else { + return container_parse_strategy::none; + } + + } else if constexpr (strategy.is_writable && strategy.kind == branch_kind::new_element) { + return container_parse_strategy::as_part; + + } else if constexpr ( + strategy.is_writable && strategy.kind == branch_kind::range && + planner::node_write_strategy_of>.kind == branch_kind::whole + ) { + return container_parse_strategy::as_part; + + } else if constexpr (parser_traits::template accepts_container) { + return container_parse_strategy::container_itself; + + } else if constexpr (strategy.is_writable) { + return container_parse_strategy::as_part; + + } else { + return container_parse_strategy::none; + } +}(); + +// The part of `attr` which a parser yielding `ParserAttr` is given, following the selections of +// `write_rank`: an existing part is referred to as it is (never cleared), a missing one (a +// disengaged optional, another alternative of a variant) is constructed by default. A list-like +// parser appends into the container referred to this way (`list_like_parser::chunk_buffer`). +template +struct ref_or_init_attribute_fn { - using alternative_type = variant_find_holdable_type< - typename parser_traits::attribute_type, - Container - >::type; - - static constexpr bool value = - parser_traits::template handles_container && - !requires (Container& c, alternative_type&& v) { - traits::push_back(c, std::move(v)); - }; + template + [[nodiscard]] static constexpr auto& operator()(Attr& attr) + { + using planner::branch_kind; + + using S = planner::storage_t; + S& s = iris::unwrap_recursive(attr); + + constexpr planner::node_write_strategy strategy = planner::node_write_strategy_of< + planner::write_node> + >; + + if constexpr (!strategy.is_writable) { + return s; + + } else if constexpr (strategy.alternative_index != planner::no_index) { + if constexpr (std::is_default_constructible_v>) { + if (auto* const existing_alt = iris::get_if(&s)) { + return ref_or_init_attribute_fn{}(*existing_alt); + } + return ref_or_init_attribute_fn{}(s.template emplace()); + + } else { + return s; + } + + } else if constexpr (strategy.kind == branch_kind::engage) { + if constexpr (std::is_default_constructible_v) { + if (!s) { + s.emplace(); + } + return ref_or_init_attribute_fn{}(*s); + + } else { + return s; + } + + } else if constexpr (strategy.kind == branch_kind::single_slot) { + return ref_or_init_attribute_fn{}(alloy::get<0>(s)); + + } else { + return s; + } + } }; +template +inline constexpr ref_or_init_attribute_fn ref_or_init_attribute_for{}; + +// A new element is parsed into in place only where its write converts nothing. A plain object created on +// the way then has the type of the value, so the parser writes into it as it would into a temporary of +// that type, from which the object is otherwise moved. +template +concept parses_into_new_element = + ( + planner::node_write_strategy_of>.kind == planner::branch_kind::new_element || + ( + planner::node_write_strategy_of>.kind == planner::branch_kind::range && + planner::node_write_strategy_of>.kind == planner::branch_kind::whole + ) + ) && + std::is_default_constructible_v> && + planner::graph_of< + planner::write_node>, V> + >::solution.result.rank >= write_rank::structural; + template struct parse_into_container_impl_default { + // parse the value and write it into `container` as a part: into a new element directly + // where the element takes the value by its shape, else through a temporary + template Se, class Context, traits::X4Container ContainerAttr> + static constexpr bool parse_part(Parser const& parser, It& first, Se const& last, Context& ctx, ContainerAttr& container) + { + using attribute_type = parser_traits::attribute_type; + using container_type = planner::storage_t; + using element_type = iris::container::element_t; + + if constexpr (parses_into_new_element>) { + element_type value{}; + if (!parser.parse(first, last, ctx, detail::ref_or_init_attribute_for(value))) return false; + iris::container::append(container, std::move(value)); + + } else { + attribute_type attr{}; + if (!parser.parse(first, last, ctx, attr)) return false; + planner::write_part(container, std::move(attr)); + } + return true; + } + + template Se, class Context, traits::X4Container ContainerAttr> + static constexpr bool parse_written_part(Parser const& parser, It& first, Se const& last, Context& ctx, ContainerAttr& container) + { + detail::action_slot::attribute_type> slot; + if (!parser.parse(first, last, ctx, slot)) return false; + + if (slot.is_generated()) { + detail::write_slot_value::size >= 2>(slot, [&container](V&& value) { + planner::write_part(container, std::forward(value)); + }); + } + return true; + } + + // pass `container` itself to the parser, which appends into it + template Se, class Context, traits::X4Container Container> + static constexpr bool parse_container(Parser const& parser, It& first, Se const& last, Context& ctx, Container& container) + { + return parser.parse(first, last, ctx, container); + } + template Se, class Context, X4NonUnusedAttribute Attr> static constexpr bool call(Parser const& parser, It& first, Se const& last, Context& ctx, Attr& attr) { using unwrapped_attribute_type = iris::unwrap_recursive_t; auto& unwrapped_attr = iris::unwrap_recursive(attr); - if constexpr (traits::is_container_v) { // Attr is a container - if constexpr (parser_accepts_container::value) { - // `Parser` accepts the exact `Container`; let parser append directly - auto&& appender = x4::make_container_appender(unwrapped_attr); - return parser.parse(first, last, ctx, appender); + if constexpr (traits::X4Container) { // Attr is a container + constexpr container_parse_strategy strategy = container_parse_strategy_for; + static_assert( + strategy != container_parse_strategy::none, + "The value of this parser cannot be added to the container, as a new element, part by part, or as a range. " + "A new element of a plain type must be constructible from the value. Note: a default constructor and an assignment " + "are not enough." + ); + if constexpr (strategy == container_parse_strategy::as_part_if_written) { + return parse_into_container_impl_default::parse_written_part(parser, first, last, ctx, unwrapped_attr); + + } else if constexpr (strategy == container_parse_strategy::container_itself) { + return parse_into_container_impl_default::parse_container(parser, first, last, ctx, unwrapped_attr); } else { - // `Parser` DOES NOT accept the exact `Container`; parse into `value_type` and append it. - typename traits::container_value::type value{}; // value-initialize - if (!parser.parse(first, last, ctx, value)) return false; - traits::push_back(unwrapped_attr, std::move(value)); - return true; + return parse_into_container_impl_default::parse_part(parser, first, last, ctx, unwrapped_attr); } } else { if constexpr (SingleElementTupleLike) { - // attribute is a single-element tuple-like; unwrap and try again - return parse_into_container_impl_default::call(parser, first, last, ctx, alloy::get<0>(unwrapped_attr)); + // Unwrap and try again + return parse_into_container_impl_default::call(parser, first, last, ctx, alloy::get<0>(unwrapped_attr)); + } else { static_assert(false, "[BUG] parse_into_container accepts a container, a variant of container or a single-element tuple-like of container"); return false; @@ -96,7 +257,7 @@ struct parse_into_container_impl_default } }; -// internal customization point +// Internal customization point template struct parse_into_container_impl : parse_into_container_impl_default @@ -121,14 +282,19 @@ parse_into_container(Parser const& parser, It& first, Se const& last, Context co using attribute_type = parser_traits::attribute_type; // e.g. `std::string` when the attribute_type is `char` - using substitute_type = variant_find_holdable_type::type>::type; - - // instead of creating a temporary `substitute_type`, append directly into the emplaced alternative - auto& variant_alt = attr.template emplace(); + using container_type = traits::default_container::type; + static_assert( + variant_has_alternative_for_v, + "The variant has no alternative which can hold the container of the parser's attribute" + ); + // append directly into the alternative, held or else emplaced, instead of into a temporary + constexpr std::size_t alt_index = variant_alternative_for_v; + auto* const existing_alt = iris::get_if(&attr); + auto& variant_alt = existing_alt ? *existing_alt : attr.template emplace(); return parse_into_container_impl::call(parser, first, last, ctx, variant_alt); } else { - static_assert(traits::is_container_v); + static_assert(traits::X4Container); return parse_into_container_impl::call(parser, first, last, ctx, attr); } } diff --git a/include/iris/x4/core/detail/parse_sequence.hpp b/include/iris/x4/core/detail/parse_sequence.hpp index 0430da00f..c8bec928b 100644 --- a/include/iris/x4/core/detail/parse_sequence.hpp +++ b/include/iris/x4/core/detail/parse_sequence.hpp @@ -15,11 +15,10 @@ #include #include #include -#include +#include #include #include -#include #include #include @@ -29,7 +28,7 @@ #include #include -#include +#include // IWYU pragma: keep namespace iris::x4 { @@ -69,6 +68,23 @@ struct sequence_layout }(); }; +template + requires (sequence_layout::attributed_count == 1) +struct may_leave_attribute_unwritten> + : may_leave_attribute_unwritten::single_attributed_index, Ps...>> +{}; + +template + requires + (sequence_layout::attributed_count == 1) && + std::same_as< + typename get_attribute_type>::type, + typename get_attribute_type::single_attributed_index, Ps...>>::type + > +struct attribute_candidates> + : attribute_candidates::single_attributed_index, Ps...>> +{}; + template struct sequence_passes_view : std::false_type {}; @@ -112,12 +128,10 @@ struct parse_sequence_tuple return elem.parse(first, last, ctx, unused); } else if constexpr (SingleElementTupleLikeView && !sequence_passes_view::value) { - auto&& elem_attr = x4::make_container_appender(alloy::get<0>(attr)); - return elem.parse(first, last, ctx, elem_attr); + return elem.parse(first, last, ctx, alloy::get<0>(attr)); } else { - auto&& elem_attr = x4::make_container_appender(attr); - return elem.parse(first, last, ctx, elem_attr); + return elem.parse(first, last, ctx, attr); } } else { @@ -125,8 +139,7 @@ struct parse_sequence_tuple return elem.parse(first, last, ctx, unused); } else if constexpr (sequence_size == 1 && !sequence_passes_view::value) { - auto&& elem_attr = x4::make_container_appender(alloy::get(attr)); - return elem.parse(first, last, ctx, elem_attr); + return elem.parse(first, last, ctx, alloy::get(attr)); } else { auto slice = [&](std::index_sequence) { @@ -231,18 +244,30 @@ struct parse_into_container_impl> Context const& ctx, Attr& attr ) { - if constexpr (traits::is_container_v) { - constexpr bool sequence_attribute_can_directly_hold_value_type = can_hold_v< - typename parser_traits>::attribute_type, - typename traits::container_value::type + if constexpr (traits::X4Container) { + // The whole sequence yields one element when its value is written into a new element + // (nothing is left behind when a later part fails); otherwise each element of the + // sequence writes into the container on its own, as the value is written part by part. + // + // Note: A sequence that may succeed without writing its value does not yield a new element + // (see `container_parse_strategy`). + using value_type = planner::model_value_t>::attribute_type>; + constexpr planner::node_write_strategy strategy = planner::node_write_strategy_of< + planner::sequence_part_node, value_type> >; - if constexpr (sequence_attribute_can_directly_hold_value_type) { - return parse_into_container_impl_default>::call(seq, first, last, ctx, attr); + if constexpr ( + strategy.is_writable && strategy.kind == planner::branch_kind::new_element && + !may_leave_attribute_unwritten_v> + ) { + return parse_into_container_impl_default>::parse_part(seq, first, last, ctx, attr); } else { - auto&& appender = x4::make_container_appender(x4::assume_container(attr)); - return detail::parse_sequence(seq, first, last, ctx, appender); + static_assert( + parser_traits>::template accepts_container, + "No element of this sequence can write into the container, nor can the sequence as a whole" + ); + return detail::parse_sequence(seq, first, last, ctx, attr); } } else { diff --git a/include/iris/x4/core/list_like_parser.hpp b/include/iris/x4/core/list_like_parser.hpp index 1294834bd..39df6ce2d 100644 --- a/include/iris/x4/core/list_like_parser.hpp +++ b/include/iris/x4/core/list_like_parser.hpp @@ -9,7 +9,6 @@ #include // export #include // IWYU pragma: export #include -#include #include #include @@ -18,6 +17,7 @@ #include #include +#include namespace iris::x4 { @@ -25,71 +25,50 @@ namespace list_like_parser { namespace detail { -template - // non-variant `ExposedAttr` -struct unwrap_container_candidate -{ - using type = unwrap_single_element_t< - unwrap_recursive_t< - typename unwrap_container_appender::type - > - >; -}; - -template - requires is_variant_v> -struct unwrap_container_candidate -{ - using type = variant_find_holdable_type< - unwrap_recursive_t, ParserAttr - >::type; -}; - template struct chunk_buffer_impl { - using type = unwrap_container_candidate::type; - static_assert(traits::X4Container::type>); + using type = std::remove_cvref_t(std::declval()))>; + static_assert( + !is_variant_v, + "The variant has no alternative which can hold the container of the parser's attribute" + ); + static_assert(traits::X4Container); }; } // detail +// The container which a list-like parser yielding `ParserAttr` appends into, as +// `x4::detail::ref_or_init_attribute_for` refers to it in `ExposedAttr` template using chunk_buffer = detail::chunk_buffer_impl::type; - +// A repetition writes its whole value as one new element when the container takes it as-is +// (e.g., the value of `*char_` into `vector` is one string). +// +// Otherwise, each parse of the subject is written into the container as a part. template -[[nodiscard]] constexpr auto& get_container(ExposedAttr& attr) +inline constexpr bool writes_as_one_element = [] { + using container_type = planner::storage_t>; + constexpr planner::node_write_strategy strategy = planner::node_write_strategy_of< + planner::write_node> + >; + return strategy.is_writable && strategy.kind == planner::branch_kind::whole; +}(); + +template Se, class Context, X4NonUnusedAttribute ExposedAttr> +[[nodiscard]] constexpr bool parse_as_one_element(Parser const& parser, It& first, Se const& last, Context const& ctx, ExposedAttr& attr) { - using unwrapped_attr_type = unwrap_single_element_t>; - auto& unwrapped_attr = x4::unwrap_single_element(iris::unwrap_recursive(attr)); - - if constexpr (is_variant_v) { - using container_alternative = variant_find_holdable_type< - unwrapped_attr_type, ParserAttr - >::type; - - if (iris::holds_alternative(unwrapped_attr)) { - return iris::unsafe_get(unwrapped_attr); - } else { - return unwrapped_attr.template emplace(); - } - - } else { - return unwrapped_attr; - } + auto& container_attr = x4::detail::ref_or_init_attribute_for::attribute_type>(attr); + return x4::detail::parse_into_container_impl_default::parse_part(parser, first, last, ctx, container_attr); } template constexpr void successful_merge_into(ChunkBuf& chunk_buf, ExposedAttr& container_attr) { - traits::append( - container_attr, - std::make_move_iterator(traits::begin(chunk_buf)), - std::make_move_iterator(traits::end(chunk_buf)) - ); - traits::clear(chunk_buf); + iris::container::append_range(container_attr, chunk_buf | std::views::as_rvalue); + iris::container::clear(chunk_buf); } } // list_like_parser diff --git a/include/iris/x4/core/move_to.hpp b/include/iris/x4/core/move_to.hpp deleted file mode 100644 index 44bef9a55..000000000 --- a/include/iris/x4/core/move_to.hpp +++ /dev/null @@ -1,291 +0,0 @@ -#ifndef IRIS_ZZ_X4_CORE_MOVE_TO_HPP -#define IRIS_ZZ_X4_CORE_MOVE_TO_HPP - -/*============================================================================= - Copyright (c) 2001-2014 Joel de Guzman - Copyright (c) 2013 Agustin Berge - Copyright (c) 2025 Nana Sakisaka - Copyright (c) 2026 The Iris Project Contributors - - 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) -=============================================================================*/ - -#include // IWYU pragma: keep - -#include -#include -#include - -#include // IWYU pragma: keep -#include - -#include -#include -#include -#include -#include - -namespace iris::x4 { - -template -constexpr void move_to(Source&&, Dest&) = delete; // `Source` and `Dest` do not fall into any of the overload below. - -// Identical types --------------------------------------- -// -// Note that these overloads must be strictly "more constrained" -// than any of the other overloads. -// -// Such resolution should be possible even without an extra -// constraint that checks `std::is_same_v`, thanks to -// the ordinary overload resolution rules of C++. - -template -constexpr void move_to(T const&& src, T& dest) - noexcept(std::is_nothrow_assignable_v) -{ - static_assert(std::is_assignable_v); - dest = std::move(src); -} - -template -constexpr void move_to(T&& src, T& dest) - noexcept(std::is_nothrow_assignable_v) -{ - static_assert(std::is_assignable_v); - dest = std::forward(src); -} - -template -constexpr void move_to(T const& src, T& dest) - noexcept(std::is_nothrow_copy_assignable_v) -{ - static_assert(std::is_assignable_v); - dest = src; -} - -template -constexpr void move_to(T&, T&) noexcept -{ - static_assert( - !std::is_const_v, - "`x4::move_to(T const&, T const&)` is not allowed" - ); - - static_assert( - !std::is_lvalue_reference_v, - "lvalue reference detected on the `src` argument of `x4::move_to`. " - "The caller is definitely lacking `std::move` or `std::forward`. If you " - "intend to *copy* the mutable value, apply `x4::move_to(std::as_const(attr_), attr)`." - ); - // Banned: possible, but bug-prone. - // dest = std::move(src); -} - -// unused_type ------------------------------------------- -template - requires - std::same_as, unused_type> || - std::same_as, unused_type> || - std::same_as, unused_container_type> || - std::same_as, unused_container_type> -constexpr void move_to(Source&&, Dest&) noexcept -{ - // unused attribute -} - -template Se> -constexpr void move_to(It const&, Se const&, unused_container_type const&) noexcept -{ - // unused attribute -} - -template Se> -constexpr void move_to(It const&, Se const&, unused_type&) = delete; // the call site is lacking `x4::assume_container` - -template Se> -constexpr void move_to(It const&, Se const&, unused_type const&) = delete; // the call site is lacking `x4::assume_container` - -template Se> -constexpr void move_to(It const&, Se const&, unused_type&&) = delete; // the call site is lacking `x4::assume_container` - -template Se> -constexpr void move_to(It const&, Se const&, unused_type const&&) = delete; // the call site is lacking `x4::assume_container` - -// Category specific -------------------------------------- - -template Dest> - requires SingleElementTupleLike -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(noexcept(dest = std::forward_like(alloy::get<0>(std::forward(src))))) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - // TODO: preliminarily invoke static_assert to check if the assignment is valid - dest = std::forward_like(alloy::get<0>(std::forward(src))); -} - -template Dest> - requires (!SingleElementTupleLike) -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(std::is_nothrow_assignable_v) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - static_assert(std::is_assignable_v); - static_assert((!CharLike && !CharLike>) || !CharIncompatibleWith>, "Mixing incompatible char types is not allowed"); - - dest = std::forward(src); -} - -template Dest> - requires - SameSizeTupleLike && - (!SingleElementTupleLike) -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(noexcept(alloy::tuple_assign(std::forward(src), dest))) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - - // TODO: preliminarily invoke static_assert to check if the assignment is valid - - alloy::tuple_assign(std::forward(src), dest); -} - -template Dest> - requires std::is_assignable_v -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(std::is_nothrow_assignable_v) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - - // e.g. Dest is `iris::rvariant` and Source is `int` - // e.g. Dest is `iris::rvariant>` and Source is `alloy::tuple` - dest = std::forward(src); -} - -template Dest> - requires (!std::is_assignable_v) && SingleElementTupleLike -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(noexcept(dest = std::forward_like(alloy::get<0>(std::forward(src))))) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - - static_assert( - std::is_assignable_v(alloy::get<0>(std::forward(src))))>, - "Error! The destination variant (Dest) cannot hold the source type (Source)" - ); - - // forward_like is *required*, since when Source is `alloy::tuple` `alloy::get<0>(std::forward(src))` returns `int&` whereas we want `int&&` instead - - // e.g. Dest is `iris::rvariant` and Source is `alloy::tuple` - dest = std::forward_like(alloy::get<0>(std::forward(src))); -} - -template Dest> -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(std::is_nothrow_assignable_v) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - static_assert(std::is_assignable_v); - dest = std::forward(src); -} - -// Containers ------------------------------------------------- - -template -struct container_appender; - -template Se, CategorizedAttr Dest> -constexpr void -move_to(It first, Se last, Dest& dest) - // never noexcept, requires container insertion -{ - static_assert(!std::same_as, unused_type>); - static_assert(!std::same_as, unused_container_type>); - - if constexpr (CharLike>> && CharLike::type>>) { - static_assert( - std::same_as>, std::remove_cvref_t::type>>, - "Mixing incompatible char types is not allowed" - ); - } - - if constexpr (!is_ttp_specialization_of_v) { - if (!traits::is_empty(dest)) { - traits::clear(dest); - } - } - - // Be careful, this may result in converting surprisingly incompatible types, - // for example, `std::vector` and `std::set`. Such types must be - // handled *before* invoking `move_to`. - - traits::append(dest, first, last); // try to reuse underlying memory buffer -} - -template Se, CategorizedAttr Dest> - requires SingleElementTupleLike -constexpr void -move_to(It first, Se last, Dest& dest) - noexcept(noexcept(x4::move_to(first, last, alloy::get<0>(dest)))) -{ - x4::move_to(first, last, alloy::get<0>(dest)); -} - -// Move non-container `src` into container `dest`. -// e.g. Source=std::string_view, Dest=std::string (used in `fixed_value_parser`) -template Dest> - requires - (!traits::X4Container) && - (!std::same_as, unused_container_type>) -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(std::is_nothrow_assignable_v) -{ - if constexpr (is_ttp_specialization_of_v, container_appender>) { - static_assert(std::is_assignable_v::container_type&, Source>); - dest.container = std::forward(src); - } else { - static_assert(std::is_assignable_v); - dest = std::forward(src); - } -} - -template Dest> -constexpr void -move_to(Source&& src, Dest& dest) - // TODO: noexcept -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - - if constexpr (std::is_constructible_v::type, Source>) { - traits::push_back(dest, std::forward(src)); - } else { - if constexpr (std::is_rvalue_reference_v) { - x4::move_to(std::make_move_iterator(std::ranges::begin(src)), std::make_move_iterator(std::ranges::end(src)), dest); - } else { - x4::move_to(std::ranges::begin(src), std::ranges::end(src), dest); - } - } -} - -// Single-element tuple-like forwarding -template Dest> - requires SingleElementTupleLike -constexpr void -move_to(Source&& src, Dest& dest) - noexcept(noexcept(x4::move_to(std::forward(src), alloy::get<0>(dest)))) -{ - static_assert(!std::same_as, Dest>, "[BUG] This call should instead resolve to the overload handling identical types"); - - x4::move_to(std::forward(src), alloy::get<0>(dest)); -} - -} // iris::x4 - -#endif diff --git a/include/iris/x4/core/parser.hpp b/include/iris/x4/core/parser.hpp index 745d00c48..c8c9c3d1d 100644 --- a/include/iris/x4/core/parser.hpp +++ b/include/iris/x4/core/parser.hpp @@ -144,7 +144,7 @@ struct proxy_parser : unary_parser static constexpr std::size_t sequence_size = parser_traits::sequence_size; template - static constexpr bool handles_container = parser_traits::template handles_container; + static constexpr bool accepts_container = parser_traits::template accepts_container; using unary_parser::unary_parser; }; diff --git a/include/iris/x4/core/parser_traits.hpp b/include/iris/x4/core/parser_traits.hpp index f9c69a09f..292aefda7 100644 --- a/include/iris/x4/core/parser_traits.hpp +++ b/include/iris/x4/core/parser_traits.hpp @@ -11,7 +11,9 @@ #include // IWYU pragma: keep -#include +#include + +#include #include #include @@ -45,17 +47,17 @@ struct get_attribute_type }; template -struct get_handles_container +struct get_accepts_container { - static constexpr bool value = can_hold_v::type>; + static constexpr bool value = is_writable_v::type>; }; template requires - requires { Parser::template handles_container; } -struct get_handles_container + requires { Parser::template accepts_container; } +struct get_accepts_container { - static constexpr bool value = Parser::template handles_container; + static constexpr bool value = Parser::template accepts_container; }; } // detail @@ -94,6 +96,45 @@ struct get_sequence_size static constexpr std::size_t value = Parser::sequence_size; }; +template +concept attribute_passing_proxy = + requires { typename Parser::proxy_backend_type; } && + std::same_as::type, typename get_attribute_type::type>; + +// Whether the parser may succeed without writing its attribute, i.e., an alternative with a +// branch without an attribute, or a parser that passes its attribute to such an alternative. +// +// The attribute given to such a parser is NOT prepared for its attribute type (`prepare_attribute_for`), +// as the part made for that type would remain even when nothing is written (e.g., `int` emplaced +// into `rvariant` by `int_ | lit("auto")`, left as `0` when "auto" matches). +// +// Instead, the attribute is handed over whole in its default state, which then remains when +// the parser writes nothing. +template +struct may_leave_attribute_unwritten : std::false_type {}; + +template +struct may_leave_attribute_unwritten : may_leave_attribute_unwritten {}; + +template +inline constexpr bool may_leave_attribute_unwritten_v = may_leave_attribute_unwritten::value; + +template +struct attribute_candidates +{ + using type = std::conditional_t< + std::same_as::type, unused_type>, + type_list<>, + type_list::type> + >; +}; + +template +struct attribute_candidates : attribute_candidates {}; + +template +using attribute_candidates_t = attribute_candidates::type; + } // detail @@ -129,19 +170,36 @@ struct parser_traits static constexpr std::size_t sequence_size = detail::get_sequence_size::value; + // If true, X4 may pass `Container` itself to the parser in place of a new element of it, + // and the parser appends into it. Whether X4 does so is decided by the priority of writes + // into a container (`detail::container_parse_strategy_for`). template - static constexpr bool handles_container = detail::get_handles_container::value; + static constexpr bool accepts_container = detail::get_accepts_container::value; static constexpr bool has_action = Parser::has_action; static constexpr bool need_rcontext = Parser::need_rcontext; }; -// `Parser` writes into `Container`: it fills the container itself, or yields -// one element of it +namespace detail { + +// The value of `Parser` is written into `Container` as a part: a new element, part by part, or a range appended +template +concept writes_as_part = + has_attribute_v && + planner::node_write_strategy_of< + planner::sequence_part_node< + planner::storage_t, + planner::model_value_t::attribute_type> + > + >.is_writable; + +} // detail + +// `Parser` writes into `Container`: it is passed the container itself and appends into it, or its value is written into it as a part template -concept WritesIntoContainer = - parser_traits::template handles_container || - can_hold_v::type, typename parser_traits::attribute_type>; +concept writes_into_container = + parser_traits::template accepts_container || + detail::writes_as_part; } // iris::x4 diff --git a/include/iris/x4/core/traits/attribute_category.hpp b/include/iris/x4/core/traits/attribute_category.hpp index dce2a3796..cee06b9d5 100644 --- a/include/iris/x4/core/traits/attribute_category.hpp +++ b/include/iris/x4/core/traits/attribute_category.hpp @@ -70,7 +70,7 @@ struct attribute_category using type = container_tag; // The attribute category type for `unused_container_type` is - // `container_attribute`, but it does not satisfy `is_container`. + // `container_attribute`, but it does not satisfy `X4Container`. }; template @@ -95,7 +95,7 @@ struct attribute_category }; template - requires traits::is_container_v> + requires traits::X4Container> struct attribute_category { using type = container_tag; diff --git a/include/iris/x4/core/traits/can_hold.hpp b/include/iris/x4/core/traits/can_hold.hpp deleted file mode 100644 index 878764301..000000000 --- a/include/iris/x4/core/traits/can_hold.hpp +++ /dev/null @@ -1,119 +0,0 @@ -#ifndef IRIS_ZZ_X4_CORE_TRAITS_CAN_HOLD_HPP -#define IRIS_ZZ_X4_CORE_TRAITS_CAN_HOLD_HPP - -/*============================================================================= - Copyright (c) 2001-2014 Joel de Guzman - Copyright (c) 2025 Nana Sakisaka - Copyright (c) 2026 The Iris Project Contributors - - 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) -=============================================================================*/ - -#include // IWYU pragma: keep - -#include -#include - -#include - -#include - -#include -#include - -namespace iris::x4 { - -template -struct can_hold; - -template -struct is_variant; - -namespace detail { - -template>> -struct is_all_substitute_for_tuple_impl {}; - -template -struct is_all_substitute_for_tuple_impl> - : std::conjunction, alloy::tuple_element_t>...> {}; - -template -struct is_all_substitute_for_tuple : std::false_type {}; - -template - requires SameSizeTupleLike -struct is_all_substitute_for_tuple : is_all_substitute_for_tuple_impl {}; - -template -struct value_type_can_hold - : can_hold::type, typename traits::container_value::type> -{}; - -// This "implementation" exists for short-circuiting `can_hold` for certain trivial combinations -template -struct can_hold_impl : std::false_type {}; - -template - requires - alloy::is_tuple_like_v && - alloy::is_tuple_like_v -struct can_hold_impl - : detail::is_all_substitute_for_tuple -{}; - -template - requires - traits::is_container_v && - traits::is_container_v -struct can_hold_impl - : detail::value_type_can_hold -{}; - -template - requires is_variant::value && X4UnusedAttribute -struct can_hold_impl - : std::false_type -{}; - -template - requires (!is_variant::value) && X4UnusedAttribute -struct can_hold_impl - : std::false_type -{}; - -template - requires is_variant::value && (!X4UnusedAttribute) -struct can_hold_impl - : std::is_assignable -{}; - -template -struct can_hold_impl, std::optional> - : can_hold -{}; - -} // detail - -template -struct can_hold - : detail::can_hold_impl -{ - static_assert(X4Attribute); - static_assert(X4Attribute); -}; - -template -struct can_hold - : std::true_type -{ - static_assert(X4Attribute); -}; - -template -inline constexpr bool can_hold_v = can_hold::value; - -} // iris::x4 - -#endif diff --git a/include/iris/x4/core/traits/transform_attribute.hpp b/include/iris/x4/core/traits/transform_attribute.hpp deleted file mode 100644 index 7b6ecc515..000000000 --- a/include/iris/x4/core/traits/transform_attribute.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#ifndef IRIS_ZZ_X4_CORE_TRAITS_TRANSFORM_ATTRIBUTE_HPP -#define IRIS_ZZ_X4_CORE_TRAITS_TRANSFORM_ATTRIBUTE_HPP - -/*============================================================================= - Copyright (c) 2001-2014 Joel de Guzman - Copyright (c) 2001-2012 Hartmut Kaiser - Copyright (c) 2025 Nana Sakisaka - Copyright (c) 2026 The Iris Project Contributors - - 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) -=============================================================================*/ - -#include // IWYU pragma: keep - -#include -#include - -#include -#include -#include - -namespace iris::x4 { - -template -struct transform_attribute -{ - static_assert(!std::is_reference_v, "Exposed cannot be a reference type"); - static_assert(!std::is_reference_v, "Transformed cannot be a reference type"); - - static_assert(std::assignable_from); - - using type = Transformed; - - [[nodiscard]] static constexpr Transformed pre(Exposed&) - noexcept(std::is_nothrow_default_constructible_v) - { - return Transformed{}; - } - - template - static constexpr void post(Exposed& val, TransformedT&& attr) - noexcept(noexcept(x4::move_to(std::forward(attr), val))) - { - x4::move_to(std::forward(attr), val); - } -}; - -template -concept Transformable = requires(Exposed& val) { - typename transform_attribute::type; - - { transform_attribute::pre(val) } - -> std::same_as::type>; - - { transform_attribute::post(val, std::declval()) }; -}; - -// Same attribute types; no transformation needed -template - requires - (!std::same_as, unused_type>) && - (!std::same_as, unused_container_type>) -struct transform_attribute -{ - static_assert(!std::is_reference_v, "Attribute cannot be a reference type"); - - using type = Attr&; - static constexpr Attr& pre(Attr& val) noexcept { return val; } - static constexpr void post(Attr&, Attr const&) noexcept {} -}; - -template<> -struct transform_attribute -{ - using type = unused_type; - static constexpr unused_type pre(unused_type) noexcept { return unused; } - static constexpr void post(unused_type, unused_type) noexcept {} -}; - -template<> -struct transform_attribute -{ - using type = unused_container_type; - static constexpr unused_container_type pre(unused_container_type) noexcept { return unused_container; } - static constexpr void post(unused_container_type, unused_container_type) noexcept {} -}; - -template - requires (!std::same_as) -struct transform_attribute - : transform_attribute -{ - static_assert(!std::is_reference_v, "Transformed cannot be a reference type"); -}; - -template -struct transform_attribute - : transform_attribute -{ - static_assert(!std::is_reference_v, "Transformed cannot be a reference type"); -}; - -template - requires (!std::same_as) -struct transform_attribute - : transform_attribute -{ - static_assert(!std::is_reference_v, "Transformed cannot be a reference type"); -}; - -template -struct transform_attribute - : transform_attribute -{ - static_assert(!std::is_reference_v, "Transformed cannot be a reference type"); -}; - -} // iris::x4 - -#endif diff --git a/include/iris/x4/core/traits/tuple_traits.hpp b/include/iris/x4/core/traits/tuple_traits.hpp index 95c8793d7..951c2ec42 100644 --- a/include/iris/x4/core/traits/tuple_traits.hpp +++ b/include/iris/x4/core/traits/tuple_traits.hpp @@ -14,6 +14,7 @@ #include +#include #include #include @@ -39,6 +40,11 @@ concept SingleElementTupleLikeView = SingleElementTupleLike && alloy::TupleLi namespace detail { +template +concept holds_as_single_element = + SingleElementTupleLike && + std::same_as, Declared>; + template struct unwrap_single_element_impl { diff --git a/include/iris/x4/core/traits/variant_class.hpp b/include/iris/x4/core/traits/variant_class.hpp index 4c1aa0195..b0f92f709 100644 --- a/include/iris/x4/core/traits/variant_class.hpp +++ b/include/iris/x4/core/traits/variant_class.hpp @@ -11,6 +11,8 @@ #include // IWYU pragma: keep #include +#include +#include #include @@ -25,6 +27,13 @@ inline constexpr bool is_variant_v = is_variant::value; template struct is_variant> : std::true_type {}; +// `U` is an alternative of the variant `T`, `recursive_wrapper` unwrapped +template +inline constexpr bool has_alternative_of = false; + +template +inline constexpr bool has_alternative_of, U> = is_in_v...>; + } // iris::x4 #endif diff --git a/include/iris/x4/core/traits/variant_traits.hpp b/include/iris/x4/core/traits/variant_traits.hpp index 4ad34b338..7c2cb1555 100644 --- a/include/iris/x4/core/traits/variant_traits.hpp +++ b/include/iris/x4/core/traits/variant_traits.hpp @@ -2,8 +2,6 @@ #define IRIS_ZZ_X4_CORE_TRAITS_VARIANT_TRAITS_HPP /*============================================================================= - Copyright (c) 2001-2014 Joel de Guzman - Copyright (c) 2025 Nana Sakisaka Copyright (c) 2026 The Iris Project Contributors Distributed under the Boost Software License, Version 1.0. (See accompanying @@ -12,95 +10,42 @@ #include // IWYU pragma: keep -#include -#include +#include #include +#include -#include #include -namespace iris::x4 { +#include // IWYU pragma: keep -namespace detail { +namespace iris::x4::detail { -template -struct any_of_unwrapped_exactly_same; +// The index of the alternative of `Variant` that `write_rank` selects for writing a `Value`. +// No `::value` if the write is rejected or does not go into a single alternative (a variant value +// written alternative by alternative). +// +// Note: This is an index, rather than a type, as a type may appear twice. +template +struct variant_alternative_for {}; -template -struct any_of_unwrapped_exactly_same - : std::false_type -{}; - -template - requires std::same_as> -struct any_of_unwrapped_exactly_same - : std::true_type -{}; - -template - requires (!std::same_as>) -struct any_of_unwrapped_exactly_same - : any_of_unwrapped_exactly_same -{}; - -template -struct variant_find_holdable_type_impl; - -template -struct variant_find_holdable_type_impl -{ - using type = T; -}; - -template -struct variant_find_holdable_type_impl +template + requires + (planner::node_write_strategy_of>>.is_writable) && + (planner::node_write_strategy_of>>.alternative_index != planner::no_index) +struct variant_alternative_for { - using type = std::conditional_t< - can_hold_v, T>, - - // Given some type `T`, when both `T` and `recursive_wrapper` is seen - // during attribute resolution, X4 should ideally materialize the latter - // because: - // - It means that the user has supplied at least one explicit type - // (i.e. exposed attribute type, possibly a rule attribute type) that - // is `recursive_wrapper`, and - // - constructing `T` and then moving it to `recursive_wrapper` - // involves copying from stack to heap. - // - First, // no need to unwrap due to the reason described above - - typename variant_find_holdable_type_impl::type - >; + static constexpr std::size_t value = planner::node_write_strategy_of< + planner::write_node> + >.alternative_index; }; -} // detail - +template +inline constexpr std::size_t variant_alternative_for_v = variant_alternative_for::value; -template -struct variant_find_holdable_type; - -template -struct variant_find_holdable_type -{ - static_assert(is_variant_v); - using type = Variant; -}; - -template - requires (!std::same_as, T>) && detail::any_of_unwrapped_exactly_same::value -struct variant_find_holdable_type, T> -{ - using type = T; -}; - -template - requires (!std::same_as, T>) && (!detail::any_of_unwrapped_exactly_same::value) -struct variant_find_holdable_type, T> -{ - using type = detail::variant_find_holdable_type_impl::type; -}; +template +inline constexpr bool variant_has_alternative_for_v = requires { variant_alternative_for::value; }; -} // iris::x4 +} // iris::x4::detail #endif diff --git a/include/iris/x4/core/traits/write_rank.hpp b/include/iris/x4/core/traits/write_rank.hpp new file mode 100644 index 000000000..4ca8dda15 --- /dev/null +++ b/include/iris/x4/core/traits/write_rank.hpp @@ -0,0 +1,1306 @@ +#ifndef IRIS_ZZ_X4_CORE_TRAITS_WRITE_RANK_HPP +#define IRIS_ZZ_X4_CORE_TRAITS_WRITE_RANK_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +// Important notice: part of this code was generated by an LLM and went through +// more than 120 hours of intensive review by the current maintainer. The LLM was +// used solely to generate the mathematical algorithm (the graph solver) that +// deduces how an attribute is handled. +// +// The semantics behind the algorithm were provided entirely by the maintainer, +// through a dedicated analysis of the existing codebase. While developing the +// theory, the maintainer thoroughly reviewed *every* test case in every `.cpp` +// file, which together carry more than 20 years of history going back to the +// original library. + +#include // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep + +namespace iris::x4 { + +// How `x4::write_attribute` writes a value into an attribute; the better compares greater +enum class write_rank : unsigned char +{ + // Not writable. + none, + + // Written, somewhere inside, by an assignment or a construction of the language + // which converts the value without narrowing. + assignable_without_narrowing, + + // Written by its shape: appended into a container, member by member, into + // the content of an optional or an alternative of a variant. + structural, + + // Assigned from the same plain type. + exact, +}; + +namespace detail { + +template +struct write_rank_impl; + +} // detail + +// How a value of type `V` is written into the attribute of type `SRef`. +// `SRef` must be `S&` for a non-const `S`, as the left operand of `iris::weakly_assignable_from` is. +// `V` is the right operand. +template +inline constexpr write_rank write_rank_v = detail::write_rank_impl::value; + +template +inline constexpr bool is_writable_v = write_rank_v != write_rank::none; + +namespace planner { + +inline constexpr std::size_t no_index = static_cast(-1); + +// A node is W(S, V), E(S, x) or P(S, y); its branches are in the order of priority. A branch holds +// its condition and the edges to its children. + +// W(S, V): the write of `V` into `S` +template +struct write_node +{ + using storage_type = S; + using value_type = V; +}; + +// E(S, x): an element `x` of a range appended into the container `S` +template +struct range_element_node +{ + using storage_type = S; + using value_type = V; +}; + +// P(S, y): a part `y` of a sequence, or the value of one parse of a repetition, appended into the container `S` +template +struct sequence_part_node +{ + using storage_type = S; + using value_type = V; +}; + +// A storage is an object type; a value is the type of an expression (`T` for an rvalue, `T&` or +// `T const&` for an lvalue). `recursive_wrapper` is unwrapped. +template +using storage_t = unwrap_recursive_t>; + +template +using model_value_t = std::conditional_t< + std::is_lvalue_reference_v, + unwrap_recursive_t>&, + unwrap_recursive_t> +>; + +enum class branch_kind : unsigned char +{ + assign_same, // `s = v` of the same plain type + assign, // `s = v` with a conversion + transparent, // `-a`: nothing if absent, else the content + slots, // member by member + single_slot, // into the element of a single-element tuple-like + engage_optional, // an optional from an optional + engage, // into the content of an optional + whole, // the whole range as one element + append_all, // the elements of the same type at once + each, // element by element + same_alternative, + conversion, // into the plain alternative selected as P0608 + structural, // into the alternative written by its shape + wrapping, // into the element of the single-element tuple-like alternative + wrapping_many, // two or more single-element tuple-like alternatives: ambiguous + split, // alternative by alternative of the value + new_element, + parts, + range, +}; + +// ----------------------------------------------------------------- + +namespace detail { + +// A part of a value, passed on as `std::forward_like` does +template +using part_value_t = model_value_t(std::declval())))>; + +template +using slot_value_t = part_value_t(std::declval()))>; + +template +using content_value_t = part_value_t())>; + +// An element of a range value as X4 takes it out: moved from an rvalue +template +using range_element_value_t = model_value_t, + std::ranges::range_reference_t, + std::ranges::range_rvalue_reference_t +>>; + +// A range whose element is of its own type (`std::filesystem::path`) is not a range +// here, as `[format.range.fmtkind]` does not format it as one +template +concept range_value = + (std::same_as>, container_tag> || std::same_as>, plain_tag>) && + std::ranges::forward_range> && + !std::same_as>, std::remove_cvref_t>; + +// Characters of one type are never written as characters of another +template +concept both_char_like = CharLike && CharLike; + +// `s = v` for a plain `S`. Only a narrowing of the implicit conversion into `S` itself is rejected; one into the +// parameter of a user-defined conversion or assignment operator (`Port <- long long`) is the responsibility of the type +template +concept plain_writable = + is_assignable_without_narrowing_v && + !x4::detail::dangles && + !both_char_like>; + +// `T` is constructible from the value itself, by direct-initialization where the value converts implicitly +// without narrowing; a type other than plain only from the same type +template +concept constructible_from_value = + (std::same_as, T> && std::is_convertible_v) || + ( + !std::same_as, T> && + std::same_as, plain_tag> && + is_convertible_without_narrowing_v && + std::is_constructible_v && + !x4::detail::dangles && + !both_char_like> + ); + +// Mandated where a write reaches a variant, or makes a new one. Otherwise, whether a new variant can +// be made would depend on the order of its alternatives, and would silently change the candidates. +template> +inline constexpr bool mandates_variant_default_state = true; + +template +inline constexpr bool mandates_variant_default_state = [] { + static_assert( + std::is_default_constructible_v, + "The variant attribute has no default state, as its first alternative is not default constructible. " + "Reorder the alternatives so that a default constructible one comes first." + ); + return true; +}(); + +// A new `T` can be made from the value: +// - A variant is constructed holding the alternative its write selects. It must have the default state +// (mandated), though the state is not used. +// - A plain `T` is constructed from the value (`T t(v);`, never `T t; t = v;`). +// - A `T` of the type of the value is constructed from the value. +// - Any other `T` must be default constructible. +template +concept makeable = + (std::same_as, variant_tag> && mandates_variant_default_state) || + (!std::same_as, plain_tag> && std::is_default_constructible_v) || + constructible_from_value; + +// The elements of the range value `range` to be appended, moved from unless the value is an lvalue +template +[[nodiscard]] constexpr auto appended_elements(std::remove_reference_t& range) noexcept +{ + if constexpr (std::is_lvalue_reference_v) { + return std::views::all(range); + + } else { + return range | std::views::as_rvalue; + } +} + +// A descent edge moves the value to a part of it; any other edge is a zero edge +template +struct edge +{ + using node = Node; + static constexpr bool descent = Descent; +}; + +template +struct branch {}; + +// The candidates at one position, of which the one applying is selected; ambiguous if two apply +template +struct branch_group {}; + +template +inline constexpr bool is_const_part = std::is_const_v>>; + +template> +inline constexpr bool has_const_part = false; + +template +inline constexpr bool has_const_part = [](std::index_sequence) { + return (is_const_part> || ...); +}(std::make_index_sequence>{}); + +template +inline constexpr bool has_const_part = is_const_part; + +template +inline constexpr bool has_const_part, variant_tag> = (is_const_part || ...); + +// Mandated where a write reaches `S` +template +inline constexpr bool mandates_no_const_part = [] { + static_assert( + !has_const_part, + "The attribute has a const part (a member, an element of a tuple, the content of an optional " + "or an alternative of a variant), into which no value is ever written" + ); + return true; +}(); + +template +inline constexpr std::type_identity> branch_list{}; + +// `-a`: nothing if absent, else the content +template +using transparent_branch = branch>, true>>; + +// The conditions are concepts so that the heavier checks are skipped once the element types decide them + +// The range becomes one new element as a whole (`std::vector <- std::vector`) +template +concept writes_whole_as_element = + !std::same_as>, std::remove_cvref_t>> && + makeable>, V> && + iris::container::appendable>; + +// The elements of the same type are appended at once +template +concept appends_all = + std::same_as>, std::remove_cvref_t>> && + requires(S& s, std::remove_reference_t& range) { + iris::container::append_range(s, detail::appended_elements(range)); + }; + +// W(S, V) for a variant `S`: the same type, a conversion as P0608 (`std::variant v; v = 42;` +// holds `long long`), the shape, then wrapped; X4 wraps only where it chooses the alternative, as C++: +// `Paren p = {1};` is well-formed, and `std::variant v = 1;` is not + +template +concept variant_candidate = + (Kind == branch_kind::same_alternative && std::same_as>) || + ( + Kind == branch_kind::conversion && + std::same_as, plain_tag> && + !std::same_as> && + plain_writable && + makeable + ) || + ( + Kind == branch_kind::structural && + !std::same_as> && + ( + std::same_as, container_tag> || + std::same_as, optional_tag> || + std::same_as, variant_tag> || + SameSizeTupleLike + ) + ) || + ( + Kind == branch_kind::wrapping && + SingleElementTupleLike && + !std::same_as> && + std::is_default_constructible_v && + is_convertible_without_narrowing_v>> + ); + +// The branches of `Kind` from the candidates among `Ss` +template +consteval auto variant_branches(std::index_sequence) +{ + using candidates = concat_type_list, V>, + type_list>, + type_list<> + >...>::type; + + return [](type_list...>) { + if constexpr (Kind == branch_kind::same_alternative) { + return branch_list>, V>, + edge>, V>, false> + >...>; + + } else if constexpr (Kind == branch_kind::conversion) { + // The imaginary function F_j(S_j) of each candidate, called with the value + using overloads = conversion_overloads>>...>; + + if constexpr (conversion_resolves) { + // exactly one candidate is selected + constexpr std::size_t selected = ((conversion_selects ? Js : 0) + ... + 0); + return branch_list>; + + } else { + return branch_list>; + } + + } else if constexpr (Kind == branch_kind::structural) { + return branch_list>, V>, + edge>, V>, false> + >...>>; + + } else if constexpr (sizeof...(Js) == 1) { + constexpr std::size_t J = cpack_indexing_v<0, Js...>; + using wrapper = storage_t>; + + // the child is the element, so the wrapper is checked here + static_assert(mandates_no_const_part); + + using element = storage_t>; + + // A value is wrapped only when it becomes the element as it is, or by a standard conversion + return branch_list> || (std::is_scalar_v> && std::is_scalar_v), + edge, false> + >>; + + } else if constexpr (sizeof...(Js) >= 2) { + return branch_list>; + + } else { + return branch_list>; + } + }(candidates{}); +} + +template +using variant_branches_t = decltype(detail::variant_branches(std::index_sequence_for{}))::type; + +// The branches of W(S, V) +template +consteval auto write_branches() +{ + using SCategory = attribute_category_t; + using VCategory = attribute_category_t>; + + if constexpr (std::same_as) { + if constexpr (std::same_as) { + return branch_list>; + + } else if constexpr (std::same_as, S>) { + return branch_list>>; + + } else { + return branch_list>>; + } + + } else if constexpr (std::same_as) { + // A container is appended with a range only. One integer makes a container in more than one + // way (`std::vector(3)` has 3 elements, `{3}` one, and `std::vector{3}` + // three), and C++ rejects every implicit form (`std::vector v = std::size_t{3};`). + if constexpr (std::same_as) { + return branch_list>; + + } else if constexpr (range_value) { + return branch_list< + branch, edge>, V>, false>>, + branch>, + branch>, true>> + >; + + } else { + return branch_list<>; + } + + } else if constexpr (std::same_as) { + if constexpr (SameSizeTupleLike) { + return [](std::index_sequence) { + return branch_list>, slot_value_t>, true>... + >>; + }(std::make_index_sequence>{}); + + } else if constexpr (alloy::tuple_size_v == 1) { + return branch_list>, V>, false>>>; + + } else if constexpr (std::same_as) { + return branch_list>; + + } else { + return branch_list<>; + } + + } else if constexpr (std::same_as) { + using content = storage_t; + + if constexpr (std::same_as) { + return branch_list>, + edge>, true> + >>; + + } else { + return branch_list, edge, false>>>; + } + + } else if constexpr (std::same_as) { + return [](std::type_identity>) { + using same = variant_branches_t; + + if constexpr (std::same_as) { + return std::type_identity>>::type>{}; + + } else if constexpr (std::same_as) { + // alternative by alternative of the value + return [](std::index_sequence) { + return std::type_identity(std::declval()))>>, true>...>>, + variant_branches_t + >::type>{}; + }(std::make_index_sequence>>{}); + + } else { + return std::type_identity, + variant_branches_t, + variant_branches_t + >::type>{}; + } + }(std::type_identity{}); + + } else { + return branch_list<>; + } +} + +// The new element of E(S, x) and P(S, y), made from the value +template +using new_element_branch = branch< + branch_kind::new_element, no_index, + makeable>, V> && iris::container::appendable>, + edge>, V>, false> +>; + +// The branches of E(S, x) and P(S, y): New, then Parts for a sequence value; an optional value through +// its content; a range value of P(S, y) appended as a range +template class Node, class S, class V> +consteval auto part_branches() +{ + using element = storage_t>; + + // An optional value is written through its content, unless the element takes it as it is + if constexpr ( + std::same_as>, optional_tag> && + !std::same_as, optional_tag> && + !has_alternative_of> + ) { + return branch_list>, true>>>; + + } else if constexpr (std::same_as>, tuple_tag>) { + // The parts are written through P(S, y), also from E(S, x) + return [](std::index_sequence) { + return branch_list< + new_element_branch, + branch>, true>...> + >; + }(std::make_index_sequence>>{}); + + } else if constexpr (!std::same_as, range_element_node> && range_value) { + return branch_list, false>>>; + + } else { + return branch_list>; + } +} + +template +struct branches_of; + +template +struct branches_of> +{ + static_assert(!std::is_reference_v && !std::is_const_v, "[BUG] A storage is an object type"); + static_assert(mandates_no_const_part); + static_assert(mandates_variant_default_state); + + using type = decltype(detail::write_branches())::type; +}; + +template class Node, class S, class V> +struct branches_of> +{ + using type = decltype(detail::part_branches())::type; +}; + +// The graph of the nodes reachable from a root, one rule instantiation per explored node. A node is +// explored branch by branch in the order of priority, only as far as its selection needs: first up +// to the first branch which may apply, then one such branch more at a time while the explored +// graph does not make the node writable. Writability, the usable candidates and the selections are +// computed over arrays. + +// A branch, or a candidate of a group +struct graph_item +{ + std::size_t position = 0; + branch_kind kind = branch_kind::assign; + bool cond = false; + std::size_t alternative = no_index; + std::size_t first_edge = 0; // in the edges of the graph + std::size_t last_edge = 0; +}; + +struct graph_edge +{ + std::size_t child = 0; + bool descent = false; +}; + +// The arrays of the graph, filled node by node. The children of the node being filled are given +// in the order of its edges, so that filling a node does not depend on the graph. +struct graph_output +{ + graph_item* items = nullptr; + graph_edge* edges = nullptr; + std::size_t item_index = 0; + std::size_t edge_index = 0; + std::size_t const* children = nullptr; + + constexpr void add_edge(bool const descent) noexcept + { + edges[edge_index] = graph_edge{*children++, descent}; + ++edge_index; + } +}; + +template +struct branch_shape; + +template +struct branch_shape> +{ + static constexpr bool may_apply = Cond; + static constexpr std::size_t items = 1; + static constexpr std::size_t edges = Cond ? sizeof...(Edges) : 0; + + static constexpr void fill(graph_output& out, std::size_t position) noexcept + { + out.items[out.item_index++] = graph_item{position, Kind, Cond, Alternative, out.edge_index, out.edge_index + edges}; + if constexpr (Cond) { + (out.add_edge(Edges::descent), ...); + } + } +}; + +template +struct branch_shape> +{ + static constexpr bool may_apply = (branch_shape::may_apply || ...); + static constexpr std::size_t items = sizeof...(Branches); + static constexpr std::size_t edges = (branch_shape::edges + ... + 0); + + static constexpr void fill([[maybe_unused]] graph_output& out, [[maybe_unused]] std::size_t position) noexcept + { + (branch_shape::fill(out, position), ...); + } +}; + +template +struct node_shape; + +template +struct node_shape> +{ + static constexpr std::size_t branch_count = sizeof...(Branches); + + // The prefix explored after `prefix` branches: through the next branch which may apply, or all + [[nodiscard]] static constexpr std::size_t next_prefix(std::size_t prefix) noexcept + { + constexpr bool may_apply[]{branch_shape::may_apply..., false}; + for (std::size_t i = prefix; i < branch_count; ++i) { + if (may_apply[i]) return i + 1; + } + return branch_count; + } + + [[nodiscard]] static constexpr std::size_t items_before(std::size_t prefix) noexcept + { + constexpr std::size_t counts[]{branch_shape::items..., 0}; + std::size_t items = 0; + for (std::size_t i = 0; i < prefix; ++i) { + items += counts[i]; + } + return items; + } + + [[nodiscard]] static constexpr std::size_t edges_before(std::size_t prefix) noexcept + { + constexpr std::size_t counts[]{branch_shape::edges..., 0}; + std::size_t edges = 0; + for (std::size_t i = 0; i < prefix; ++i) { + edges += counts[i]; + } + return edges; + } + + // The items and the edges of the first `Prefix` branches + template + static constexpr void fill(graph_output& out) noexcept + { + [&](std::index_sequence) { + (branch_shape>::fill(out, Is), ...); + }(std::make_index_sequence{}); + } +}; + +template +using node_shape_t = node_shape::type>; + +template +struct branch_children +{ + using type = type_list<>; +}; + +template +struct branch_children> +{ + using type = type_list; +}; + +template +struct branch_children> +{ + using type = concat_type_list::type...>::type; +}; + +// The children of the first `Prefix` branches +template::type, class Is = std::make_index_sequence> +struct explored_children; + +template +struct explored_children, std::index_sequence> +{ + using type = concat_type_list>::type...>::type; +}; + +// The children of the prefix to which a node is explored first +template +struct initial_children : explored_children::next_prefix(0)> {}; + +template +struct all_children : explored_children::branch_count> {}; + +// Breadth first, one frontier at a time, a node reaching `Children::type`: the frontier and the +// nodes found from it, other than `Seen` +template class Children, class Seen, class Frontier> +struct graph_walk; + +// IntelliSense (EDG) loses the type when a constrained partial specialization recurses into its +// primary template, so an empty frontier has its own partial specialization +template class Children, class... Seen> +struct graph_walk, type_list<>> +{ + using type = type_list<>; +}; + +template class Children, class... Seen, class Node, class... Nodes> +struct graph_walk, type_list> +{ + using added = unique_type_list< + typename concat_type_list::type, typename Children::type...>::type, + type_list + >::type; + + using type = concat_type_list< + type_list, + typename graph_walk, added>::type + >::type; +}; + +struct branch_selection +{ + std::size_t branch_index = no_index; + std::size_t alternative_index = no_index; // of the variant written into, if any + branch_kind kind = branch_kind::assign; // of the branch, or of the candidate selected + bool is_ambiguous = false; +}; + +struct graph_view +{ + std::size_t node_count = 0; + std::size_t const* first_item = nullptr; // of each node, then the end of the items + graph_item const* items = nullptr; + graph_edge const* edges = nullptr; + std::size_t edge_count = 0; + bool const* complete = nullptr; // each node explored to its last branch +}; + +struct solver_result +{ + write_rank rank = write_rank::none; // of the root + bool needs_extension = false; // a node is to be explored further + bool has_zero_cycle = false; // among the zero edges of the branches which apply +}; + +struct node_state +{ + std::size_t component = 0; // of the zero edges of the branches which apply, from 1 + std::size_t first_child = 0; // through such an edge, in `ws_.children` + std::size_t next_child = 0; + std::size_t order = 0; // of Tarjan's algorithm, from 1 + std::size_t low = 0; + std::size_t chosen_item = no_index; + std::size_t zero_parents = 0; // through the zero edges of the plan + + bool is_writable = false; + bool ok = false; // in the fixpoint being computed + bool is_derived = false; // in the least fixpoint being computed + bool is_visited = false; // by the plan +}; + +// The arrays `graph_solver` works on, allocated by the caller as the sizes are known there. +struct graph_workspace +{ + node_state* nodes = nullptr; // and one past the last, for the end of the children of the last + bool* applies = nullptr; // of each item + std::size_t* children = nullptr; // of each edge + std::size_t* stack = nullptr; // of each node + std::size_t* calls = nullptr; + std::size_t* pending = nullptr; +}; + +// The selections of the nodes of a graph and the rank of its root, the first node. Not a template: +// every graph is solved by the same function, over arrays. +class graph_solver +{ +public: + constexpr graph_solver(graph_view const& graph, graph_workspace const& ws) noexcept + : graph_(graph) + , ws_(ws) + {} + + graph_solver(graph_solver const&) = delete; + graph_solver& operator=(graph_solver const&) = delete; + + // The selections, the nodes to explore further, and the rank of the root + [[nodiscard]] constexpr solver_result solve(branch_selection* selections, bool* extend) noexcept + { + std::size_t const node_count = graph_.node_count; + solver_result result; + + find_writable(no_index, no_index); + for (std::size_t node = 0; node < node_count; ++node) { + ws_.nodes[node].is_writable = ws_.nodes[node].ok; + } + + // a node not writable by the branches explored so far may be by the others + for (std::size_t node = 0; node < node_count; ++node) { + if (!ws_.nodes[node].is_writable && !graph_.complete[node]) { + extend[node] = true; + result.needs_extension = true; + } + } + if (result.needs_extension) return result; + + // a branch applies if its condition holds and its children are writable + for (std::size_t node = 0; node < node_count; ++node) { + for (std::size_t i = graph_.first_item[node]; i != graph_.first_item[node + 1]; ++i) { + ws_.applies[i] = ws_.nodes[node].is_writable && applies(graph_.items[i]); + } + } + find_components(); + for (std::size_t node = 0; node < node_count; ++node) { + for (std::size_t i = graph_.first_item[node]; i != graph_.first_item[node + 1]; ++i) { + result.has_zero_cycle = result.has_zero_cycle || (ws_.applies[i] && cyclic(node, graph_.items[i])); + } + } + + // the selection: the first position with a usable candidate + for (std::size_t node = 0; node < node_count; ++node) { + if (!ws_.nodes[node].is_writable) continue; + + branch_selection& selection = selections[node]; + std::size_t candidates = 0; + for (std::size_t i = graph_.first_item[node]; i != graph_.first_item[node + 1]; ++i) { + graph_item const& item = graph_.items[i]; + if (selection.branch_index != no_index && item.position != selection.branch_index) break; + if (!usable(node, i)) continue; + + selection.branch_index = item.position; + selection.kind = item.kind; + selection.alternative_index = item.alternative; + selection.is_ambiguous = item.kind == branch_kind::wrapping_many || (item.kind == branch_kind::conversion && item.alternative == no_index); + ws_.nodes[node].chosen_item = i; + ++candidates; + } + if (candidates >= 2) { + selection.alternative_index = no_index; + selection.is_ambiguous = true; + } + } + + if (ws_.nodes[0].is_writable) { + result.rank = plan_rank(selections); + } + return result; + } + +private: + // The rank of the plan: the nodes reached through the selections, none of them ambiguous, and + // no cycle of the zero edges among them (a conflict of the selections) + [[nodiscard]] constexpr write_rank plan_rank(branch_selection const* selected) noexcept + { + bool converts = false; + std::size_t plan_size = 0; // in `ws_.stack` + std::size_t pending = 0; + ws_.pending[pending++] = 0; + ws_.nodes[0].is_visited = true; + while (pending != 0) { + std::size_t const node = ws_.pending[--pending]; + if (selected[node].is_ambiguous) return write_rank::none; + + ws_.stack[plan_size++] = node; + graph_item const& item = graph_.items[ws_.nodes[node].chosen_item]; + converts = converts || item.kind == branch_kind::assign || item.kind == branch_kind::conversion; + for (std::size_t e = item.first_edge; e != item.last_edge; ++e) { + graph_edge const& edge = graph_.edges[e]; + if (!edge.descent) { + ++ws_.nodes[edge.child].zero_parents; + } + if (ws_.nodes[edge.child].is_visited) continue; + + ws_.nodes[edge.child].is_visited = true; + ws_.pending[pending++] = edge.child; + } + } + + // the zero edges of the plan without a cycle: every node is removed, one without a zero parent at a time + std::size_t removed = 0; + for (std::size_t k = 0; k != plan_size; ++k) { + if (ws_.nodes[ws_.stack[k]].zero_parents == 0) { + ws_.pending[pending++] = ws_.stack[k]; + } + } + while (pending != 0) { + graph_item const& item = graph_.items[ws_.nodes[ws_.pending[--pending]].chosen_item]; + ++removed; + for (std::size_t e = item.first_edge; e != item.last_edge; ++e) { + graph_edge const& edge = graph_.edges[e]; + if (!edge.descent && --ws_.nodes[edge.child].zero_parents == 0) { + ws_.pending[pending++] = edge.child; + } + } + } + if (removed != plan_size) return write_rank::none; + + return + graph_.items[ws_.nodes[0].chosen_item].kind == branch_kind::assign_same ? write_rank::exact : + converts ? write_rank::assignable_without_narrowing : + write_rank::structural; + } + + // Writable: the greatest fixpoint over the descent edges of the least one over the zero edges; a cycle + // is written through only where the value descends (a tree flattened), never while it stays the same. + // With `restricted_node`, that node takes `restricted_item` only. + constexpr void find_writable(std::size_t restricted_node, std::size_t restricted_item) noexcept + { + std::size_t const node_count = graph_.node_count; + restricted_node_ = restricted_node; + restricted_item_ = restricted_item; + + // a node none of whose branches has its condition is never writable + for (std::size_t node = 0; node < node_count; ++node) { + ws_.nodes[node].ok = false; + for (std::size_t i = first_item(node); i != last_item(node); ++i) { + if (graph_.items[i].cond) { + ws_.nodes[node].ok = true; + break; + } + } + } + while (true) { + // the least fixpoint in place over the nodes not derived yet, the last node first as a child + // mostly follows its parent + std::size_t pending = 0; + for (std::size_t node = node_count; node-- != 0;) { + ws_.nodes[node].is_derived = false; + if (ws_.nodes[node].ok) { + ws_.pending[pending++] = node; + } + } + for (std::size_t before = pending + 1; pending != before;) { // until a pass derives none + before = pending; + pending = 0; + for (std::size_t k = 0; k != before; ++k) { + std::size_t const node = ws_.pending[k]; + if (derivable_node(node)) { + ws_.nodes[node].is_derived = true; + } else { + ws_.pending[pending++] = node; + } + } + } + if (pending == 0) break; + + for (std::size_t k = 0; k != pending; ++k) { + ws_.nodes[ws_.pending[k]].ok = false; + } + } + } + + [[nodiscard]] constexpr std::size_t first_item(std::size_t node) const noexcept + { + return node == restricted_node_ ? restricted_item_ : graph_.first_item[node]; + } + + [[nodiscard]] constexpr std::size_t last_item(std::size_t node) const noexcept + { + return node == restricted_node_ ? restricted_item_ + 1 : graph_.first_item[node + 1]; + } + + [[nodiscard]] constexpr bool derivable_node(std::size_t node) const noexcept + { + for (std::size_t i = first_item(node); i != last_item(node); ++i) { + if (derivable(graph_.items[i])) return true; + } + return false; + } + + // A child through a descent edge is in the fixpoint, and one through a zero edge derived + [[nodiscard]] constexpr bool derivable(graph_item const& item) const noexcept + { + if (!item.cond) return false; + + for (std::size_t e = item.first_edge; e != item.last_edge; ++e) { + graph_edge const& edge = graph_.edges[e]; + if (edge.descent ? !ws_.nodes[edge.child].ok : !ws_.nodes[edge.child].is_derived) return false; + } + return true; + } + + [[nodiscard]] constexpr bool applies(graph_item const& item) const noexcept + { + if (!item.cond) return false; + + for (std::size_t e = item.first_edge; e != item.last_edge; ++e) { + if (!ws_.nodes[graph_.edges[e].child].is_writable) return false; + } + return true; + } + + // A zero edge of the item returns to the node through the zero edges of the branches which apply + [[nodiscard]] constexpr bool cyclic(std::size_t node, graph_item const& item) const noexcept + { + for (std::size_t e = item.first_edge; e != item.last_edge; ++e) { + graph_edge const& edge = graph_.edges[e]; + if (!edge.descent && ws_.nodes[edge.child].component == ws_.nodes[node].component) return true; + } + return false; + } + + // A finite write takes the item at the node, the other nodes taking any of theirs + [[nodiscard]] constexpr bool usable(std::size_t node, std::size_t i) noexcept + { + if (!ws_.applies[i]) return false; + if (!cyclic(node, graph_.items[i])) return true; + + find_writable(node, i); + return ws_.nodes[node].ok; + } + + // The strongly connected components of the zero edges of the branches which apply, by Tarjan's + // algorithm without recursion + constexpr void find_components() noexcept + { + std::size_t const node_count = graph_.node_count; + + std::size_t child_count = 0; + for (std::size_t node = 0; node < node_count; ++node) { + node_state& state = ws_.nodes[node]; + state.first_child = state.next_child = child_count; + for (std::size_t i = graph_.first_item[node]; i != graph_.first_item[node + 1]; ++i) { + if (!ws_.applies[i]) continue; + + for (std::size_t e = graph_.items[i].first_edge; e != graph_.items[i].last_edge; ++e) { + if (!graph_.edges[e].descent) { + ws_.children[child_count++] = graph_.edges[e].child; + } + } + } + } + ws_.nodes[node_count].first_child = child_count; + + std::size_t stack_size = 0; + std::size_t call_count = 0; + std::size_t next_order = 1; + std::size_t next_component = 1; + for (std::size_t root = 0; root < node_count; ++root) { + if (ws_.nodes[root].order != 0) continue; + + ws_.nodes[root].order = ws_.nodes[root].low = next_order++; + ws_.stack[stack_size++] = root; + ws_.calls[call_count++] = root; + + while (call_count != 0) { + std::size_t const node = ws_.calls[call_count - 1]; + node_state& state = ws_.nodes[node]; + if (state.next_child != ws_.nodes[node + 1].first_child) { + std::size_t const child = ws_.children[state.next_child++]; + node_state& child_state = ws_.nodes[child]; + if (child_state.order == 0) { + child_state.order = child_state.low = next_order++; + ws_.stack[stack_size++] = child; + ws_.calls[call_count++] = child; + + } else if (child_state.component == 0 && child_state.order < state.low) { // on the stack + state.low = child_state.order; + } + continue; + } + + if (state.low == state.order) { + std::size_t member = no_index; + do { + member = ws_.stack[--stack_size]; + ws_.nodes[member].component = next_component; + } while (member != node); + ++next_component; + } + --call_count; + if (call_count != 0 && state.low < ws_.nodes[ws_.calls[call_count - 1]].low) { + ws_.nodes[ws_.calls[call_count - 1]].low = state.low; + } + } + } + } + + graph_view graph_; + graph_workspace ws_; + std::size_t restricted_node_ = no_index; + std::size_t restricted_item_ = no_index; +}; + +template +struct graph_solution +{ + std::array selections{}; + std::array needs_extension{}; + solver_result result; +}; + +// The indices in the graph `Nodes` of the children of a node, in the order of its edges (and one +// more element: MSVC rejects an array of no element in a constant evaluation) +template +inline constexpr std::array child_indices{}; + +template +inline constexpr std::array child_indices>{find_index_exactly_once_v..., 0}; + +template +[[nodiscard]] constexpr graph_solution solve_nodes(type_list, constant_list, std::index_sequence) +{ + constexpr std::size_t item_count = (node_shape_t::items_before(Prefixes) + ... + 0); + constexpr std::size_t edge_count = (node_shape_t::edges_before(Prefixes) + ... + 0); + + std::array first_item{}; + std::array items{}; + std::array edges{}; + std::array const complete{(Prefixes == node_shape_t::branch_count)...}; + + graph_output out{items.data(), edges.data()}; + ( + ( + first_item[Is] = out.item_index, + out.children = child_indices, typename explored_children::type>.data(), + node_shape_t::template fill(out) + ), + ... + ); + first_item[sizeof...(Nodes)] = out.item_index; + + std::array nodes{}; + std::array applies{}; + std::array children{}; + std::array stack{}; + std::array calls{}; + std::array pending{}; + + graph_solution solution; + solution.result = graph_solver( + graph_view{ + .node_count = sizeof...(Nodes), + .first_item = first_item.data(), + .items = items.data(), + .edges = edges.data(), + .edge_count = edges.size(), + .complete = complete.data(), + }, + graph_workspace{ + .nodes = nodes.data(), + .applies = applies.data(), + .children = children.data(), + .stack = stack.data(), + .calls = calls.data(), + .pending = pending.data(), + } + ).solve(solution.selections.data(), solution.needs_extension.data()); + return solution; +} + +// The graph of `Nodes`, each explored to its prefix in `Prefixes` +template> +struct explored_graph; + +template +struct explored_graph, constant_list, std::index_sequence> +{ + using nodes = type_list; + + static constexpr graph_solution solution = detail::solve_nodes(nodes{}, constant_list{}, std::index_sequence{}); + + static constexpr bool is_complete = ((Prefixes == node_shape_t::branch_count) && ...); + + using next_prefixes = constant_list<(solution.needs_extension[Is] ? node_shape_t::next_prefix(Prefixes) : Prefixes)...>; + + template + static constexpr branch_selection selection_of = solution.selections[find_index_exactly_once_v]; +}; + +// ReSharper fails to compute the graph in a constraint when a function deduces it as its return +// type, so the graph is computed by class templates only +template +struct explore; + +template +struct explore_completely; + +template +struct explore_completely> +{ + using type = explored_graph, constant_list::branch_count...>>; +}; + +// The graph after the round that explored `Graph` +template +struct next_round +{ + using type = Graph; +}; + +template +struct next_round : explore {}; + +// from the nodes of the graph, the root first +template +struct next_round : explore_completely, typename Graph::nodes>::type> {}; + +template +struct explore_round; + +template +struct explore_round, constant_list, type_list> + : next_round, constant_list::next_prefix(0)...>>> +{}; + +// The graph explored round by round: the nodes `Known`, explored to `Prefixes`, stay first in +// their order, and the nodes reached from them are explored to their first prefixes. A graph is explored +// further while a node is not writable by the branches explored so far. The usable candidates on a cycle +// of zero edges may depend on the branches not explored, so such a graph is then explored completely. +template +struct explore, constant_list> + : explore_round< + type_list, + constant_list, + typename graph_walk< + initial_children, + type_list, + typename unique_type_list::type...>::type, type_list>::type + >::type + > +{}; + +} // detail + +// The write the selection of a node performs, which the parse side follows (the part a parser +// writes into, and whether a container takes a value as a new element or appended by the parser) +struct node_write_strategy +{ + constexpr node_write_strategy(write_rank rank, detail::branch_selection const& selection) noexcept + : is_writable(rank != write_rank::none) + , kind(selection.kind) + , alternative_index(selection.alternative_index) + {} + + bool is_writable; // the write from the node is accepted, not `write_rank::none` + branch_kind kind; // of the branch selected, or of the candidate selected + std::size_t alternative_index; // of the variant written into, if any +}; + +// The graph of the nodes reachable from `Root`, the first of them, explored as far as the selections need +template +using graph_of = detail::explore, constant_list::next_prefix(0)>>::type; + +template +inline constexpr node_write_strategy node_write_strategy_of{ + graph_of::solution.result.rank, + graph_of::template selection_of +}; + +} // planner + +namespace detail { + +template + requires X4UnusedAttribute> || X4UnusedAttribute> +struct write_rank_impl +{ + static_assert(mandates_writable_ref); + + static constexpr write_rank value = write_rank::none; +}; + +template + requires (!X4UnusedAttribute> && !X4UnusedAttribute>) +struct write_rank_impl +{ + static_assert(mandates_writable_ref); + + static constexpr write_rank value = planner::graph_of< + planner::write_node, planner::model_value_t> + >::solution.result.rank; +}; + +} // detail + +} // iris::x4 + +#endif diff --git a/include/iris/x4/core/unused.hpp b/include/iris/x4/core/unused.hpp index b17efaef8..75276fc42 100644 --- a/include/iris/x4/core/unused.hpp +++ b/include/iris/x4/core/unused.hpp @@ -38,7 +38,7 @@ struct unused_type }; // The attribute category type for `unused_container_type` is -// `container_attribute`, but it does not satisfy `is_container`. +// `container_attribute`, but it does not satisfy `X4Container`. struct unused_container_type { constexpr explicit unused_container_type() noexcept = default; diff --git a/include/iris/x4/core/write_attribute.hpp b/include/iris/x4/core/write_attribute.hpp new file mode 100644 index 000000000..3b1943ae6 --- /dev/null +++ b/include/iris/x4/core/write_attribute.hpp @@ -0,0 +1,380 @@ +#ifndef IRIS_ZZ_X4_CORE_WRITE_ATTRIBUTE_HPP +#define IRIS_ZZ_X4_CORE_WRITE_ATTRIBUTE_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +#include // IWYU pragma: keep + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep + +namespace iris::x4 { + +namespace planner { + +namespace detail { + +// The plan of a node: the branch the selection in `Graph` chose, executed recursively +template +struct write_plan; + +// Constructs a new object for the value by invoking `construct(fill, args...)`, which constructs +// the object from `args` and then calls `fill` with it. +// +// - A plain type, or one of the type of the value, is constructed from the value. +// - A variant is constructed holding the alternative its write selects, which is +// made by the same rule, so that no alternative is constructed by default and +// then written into. +// - Any other type is constructed by default and written into by its shape (by `fill`). +template +constexpr void construct_node(Construct const& construct, typename NodeT::value_type&& value) +{ + using T = NodeT::storage_type; + using V = NodeT::value_type; + constexpr branch_selection selection = Graph::template selection_of; + constexpr auto fill_nothing = [](auto&) noexcept {}; + + if constexpr (constructible_from_value) { + construct(fill_nothing, std::forward(value)); + + } else if constexpr ( + std::same_as, variant_tag> && + (selection.kind == branch_kind::same_alternative || selection.kind == branch_kind::structural || selection.kind == branch_kind::conversion) + ) { + constexpr std::size_t J = selection.alternative_index; + + if constexpr (selection.kind == branch_kind::conversion) { + if constexpr (is_wrapped_alternative_v) { + construct(fill_nothing, std::in_place_index, std::in_place, std::forward(value)); + } else { + construct(fill_nothing, std::in_place_index, std::forward(value)); + } + + } else { + detail::construct_node, V>>( + [&construct](Fill const& fill, Args&&... args) { + auto const fill_alternative = [&fill](auto& variant) { fill(iris::unsafe_get(variant)); }; + if constexpr (is_wrapped_alternative_v) { + construct(fill_alternative, std::in_place_index, std::in_place, std::forward(args)...); + } else { + construct(fill_alternative, std::in_place_index, std::forward(args)...); + } + }, + std::forward(value) + ); + } + + } else if constexpr (std::same_as, variant_tag> && selection.kind == branch_kind::split) { + [&construct, &value](std::index_sequence) { + (void)( + ( + value.index() == Is && + ( + detail::construct_node(std::declval()))>>>( + construct, + std::forward_like(iris::get(value)) + ), + true + ) + ) || ... + ); + }(std::make_index_sequence>>{}); + + } else { + construct([&value](auto& object) { write_plan::apply(object, std::forward(value)); }); + } +} + +// Engage into the content of an optional if any, else into a new content that `construct_node` makes +template +constexpr void engage(Optional& s, typename NodeT::value_type&& value) +{ + using V = NodeT::value_type; + + if (s) { + write_plan::apply(iris::unwrap_recursive(*s), std::forward(value)); + return; + } + detail::construct_node( + [&s](Fill const& fill, Args&&... args) { + if constexpr (is_recursive_wrapper_v) { + fill(iris::unwrap_recursive(s.emplace(std::in_place, std::forward(args)...))); + } else { + fill(iris::unwrap_recursive(s.emplace(std::forward(args)...))); + } + }, + std::forward(value) + ); +} + +// Write into the alternative `J` if held, else into a new one that `construct_node` makes +template +constexpr void write_alternative(Variant& s, typename NodeT::value_type&& value) +{ + using V = NodeT::value_type; + + if (auto* const existing_alt = iris::get_if(&s)) { + write_plan::apply(*existing_alt, std::forward(value)); + return; + } + detail::construct_node( + [&s](Fill const& fill, Args&&... args) { + if constexpr (is_wrapped_alternative_v) { + fill(s.template emplace(std::in_place, std::forward(args)...)); + } else { + fill(s.template emplace(std::forward(args)...)); + } + }, + std::forward(value) + ); +} + +// Appends an element constructed from `args` into the container, and calls `fill` with it. +// +// - If the element can be constructed directly in the container from `args`, and written in +// place after it is appended (e.g. `std::vector`), it is constructed so. +// - In this case, the value written must not refer into the container; otherwise the +// behavior is undefined. +// +// - Otherwise, the element is made outside the container first and then appended; e.g. an +// element of `std::set`, which is const, or of a container which appends a complete element +// only (`push_back(T)`). +template +constexpr void append_element(Container& s, Fill const& fill, Args&&... args) +{ + using element_type = iris::container::element_t; + + if constexpr ( + std::same_as, element_type&> && + std::invocable + ) { + fill(iris::unwrap_recursive(iris::container::append_return(s, std::forward(args)...))); + + } else { + std::optional element; + fill(iris::unwrap_recursive(element.emplace(std::forward(args)...))); + iris::container::append(s, std::move(*element)); + } +} + +// Appends an element made by `construct_node` into the container (see `append_element`) +template +constexpr void push_new_element(Container& s, typename NodeT::value_type&& value) +{ + using T = NodeT::storage_type; + using V = NodeT::value_type; + + if constexpr (constructible_from_value) { + iris::container::append(s, std::forward(value)); + + } else { + detail::construct_node( + [&s](Fill const& fill, Args&&... args) { + if constexpr (is_recursive_wrapper_v>) { + detail::append_element(s, fill, std::in_place, std::forward(args)...); + } else { + detail::append_element(s, fill, std::forward(args)...); + } + }, + std::forward(value) + ); + } +} + +template +struct write_step; + +template +struct write_step, AlternativeI> +{ + template + using child = pack_indexing_t::node; + + template + static constexpr void apply(S& s, V&& v) + { + if constexpr (Kind == branch_kind::assign_same || Kind == branch_kind::assign) { + s = std::forward(v); + + } else if constexpr (Kind == branch_kind::transparent) { + if (v) { + write_plan>::apply(s, iris::unwrap_recursive(std::forward_like(*v))); + } + + } else if constexpr (Kind == branch_kind::slots) { + [&](std::index_sequence) { + (write_plan>::apply( + iris::unwrap_recursive(alloy::get(s)), + iris::unwrap_recursive(std::forward_like(alloy::get(v))) + ), ...); + }(std::index_sequence_for{}); + + } else if constexpr (Kind == branch_kind::single_slot) { + write_plan>::apply(iris::unwrap_recursive(alloy::get<0>(s)), std::forward(v)); + + } else if constexpr (Kind == branch_kind::engage_optional) { + if (!v) { + s.reset(); + return; + } + detail::engage>(s, iris::unwrap_recursive(std::forward_like(*v))); + + } else if constexpr (Kind == branch_kind::engage) { + detail::engage>(s, std::forward(v)); + + } else if constexpr (Kind == branch_kind::whole || Kind == branch_kind::new_element) { + detail::push_new_element>(s, std::forward(v)); + + } else if constexpr (Kind == branch_kind::append_all) { + iris::container::append_range(s, detail::appended_elements(v)); + + } else if constexpr (Kind == branch_kind::each) { + for (auto&& element : detail::appended_elements(v)) { + write_plan>::apply(s, iris::unwrap_recursive(std::forward(element))); + } + + } else if constexpr (Kind == branch_kind::same_alternative || Kind == branch_kind::structural) { + detail::write_alternative>(s, std::forward(v)); + + } else if constexpr (Kind == branch_kind::conversion) { + if (auto* const existing_alt = iris::get_if(&s)) { + *existing_alt = std::forward(v); + return; + } + s.template emplace(std::forward(v)); + + } else if constexpr (Kind == branch_kind::wrapping) { + auto* const existing_alt = iris::get_if(&s); + auto& wrapper = existing_alt ? *existing_alt : s.template emplace(); + write_plan>::apply(iris::unwrap_recursive(alloy::get<0>(wrapper)), std::forward(v)); + + } else if constexpr (Kind == branch_kind::split) { + [&](std::index_sequence) { + (void)((v.index() == Is && (write_plan>::apply(s, std::forward_like(iris::get(v))), true)) || ...); + }(std::index_sequence_for{}); + + } else if constexpr (Kind == branch_kind::parts) { + [&](std::index_sequence) { + (write_plan>::apply(s, iris::unwrap_recursive(std::forward_like(alloy::get(v)))), ...); + }(std::index_sequence_for{}); + + } else { + static_assert(Kind == branch_kind::range); + write_plan>::apply(s, std::forward(v)); + } + } +}; + +// The candidate of a group for the alternative selected +template +struct write_step, AlternativeI> +{ + template + struct chosen + { + using type = type_list<>; + }; + + template + struct chosen> + { + using type = type_list>; + }; + + template + static constexpr void apply(S& s, V&& v) + { + using branch_type = at_c_t<0, typename concat_type_list::type...>::type>; + write_step::template apply(s, std::forward(v)); + } +}; + +template class NodeTT, class S, class V> +struct write_plan> +{ + static constexpr branch_selection selection = Graph::template selection_of>; + + static constexpr void apply(S& s, V&& v) + { + using branch_type = at_c_t>::type>; + write_step::template apply(s, std::forward(v)); + } +}; + +struct write_attribute_fn +{ + template + requires X4UnusedAttribute || X4UnusedAttribute> + static constexpr void operator()(S&, V&&) noexcept + {} + + // Not `noexcept`. Deriving it from the plan is not planned; reconsider it when a concrete need arises. + // `is_writable_v` is spelled out, as ReSharper judges a constraint of that variable alone as false. + template + requires (!X4UnusedAttribute && !X4UnusedAttribute>) && (write_rank_v != write_rank::none) + static constexpr void operator()(S& s, V&& v) + { + using root = write_node, model_value_t>; + write_plan, root>::apply(iris::unwrap_recursive(s), iris::unwrap_recursive(std::forward(v))); + } +}; + +} // detail + +// Passes `v`, the attribute of `x4::rule` or `x4::as`, to the exposed attribute `s` by the ordinary +// assignment, which the caller has checked by `X4StrictlyWritable`. A container which holds +// the preceding results keeps them: the value assigned to a new container is appended to it. +template +constexpr void pass_declared_attribute(S& s, V&& v) +{ + if constexpr (traits::X4Container) { + if (!std::ranges::empty(s)) { + S assigned{}; + assigned = std::forward(v); + iris::container::append_range(s, assigned | std::views::as_rvalue); + return; + } + } + s = std::forward(v); +} + +// Writes `v`, a part of a sequence or the value of one parse of a repetition, into the container +// `s`: as a new element, part by part, or a range appended (P(S, y)) +template +constexpr void write_part(S& s, V&& v) +{ + using node = sequence_part_node, model_value_t>; + static_assert(node_write_strategy_of.is_writable, "The value is not written into the container, as a new element or otherwise"); + + detail::write_plan, node>::apply( + iris::unwrap_recursive(s), + iris::unwrap_recursive(std::forward(v)) + ); +} + +} // planner + +// Writes `v` into the attribute `s`, as `write_rank_v` rates it. A container is appended +// into, and an existing content or alternative is written into rather than replaced. +inline constexpr planner::detail::write_attribute_fn write_attribute{}; + +} // iris::x4 + +#endif diff --git a/include/iris/x4/debug/default_error_handler.hpp b/include/iris/x4/debug/default_error_handler.hpp index de251c2bd..80fe19afe 100644 --- a/include/iris/x4/debug/default_error_handler.hpp +++ b/include/iris/x4/debug/default_error_handler.hpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -232,7 +233,9 @@ class default_error_handler this->print_line_highlight(line, err_pos); } - void print_file_line(int line) const + // A template so that `std::print` is instantiated lazily + template + void print_file_line(Line const line) const { if (!error_out_) return; diff --git a/include/iris/x4/debug/print_attribute.hpp b/include/iris/x4/debug/print_attribute.hpp index 29364c23a..1c118baad 100644 --- a/include/iris/x4/debug/print_attribute.hpp +++ b/include/iris/x4/debug/print_attribute.hpp @@ -28,7 +28,7 @@ #include #include #include -#include +#include #include #include #include @@ -143,7 +143,10 @@ inline void print_chars(std::ostream& os, char32_t const ch) major_cat == unicode::properties::separator || major_cat == unicode::properties::other // TODO: handle Grapheme_Extend=Yes ) { - std::print(os, "\\u{{{:x}}}", static_cast(ch)); + char escaped[16]{'\\', 'u', '{'}; + auto const res = std::to_chars(escaped + 3, std::end(escaped) - 1, static_cast(ch), 16); + *res.ptr = '}'; + os.write(escaped, res.ptr + 1 - escaped); return; } @@ -282,8 +285,8 @@ struct print_attribute_debug } else { out << '['; bool is_first = true; - auto last = traits::end(val); - for (auto it = traits::begin(val); it != last; ++it) { + auto last = std::ranges::end(val); + for (auto it = std::ranges::begin(val); it != last; ++it) { if (is_first) { is_first = false; } else { diff --git a/include/iris/x4/directive/matches.hpp b/include/iris/x4/directive/matches.hpp index dd6ce77a6..a06b61060 100644 --- a/include/iris/x4/directive/matches.hpp +++ b/include/iris/x4/directive/matches.hpp @@ -13,7 +13,7 @@ =============================================================================*/ #include -#include +#include #include #include @@ -42,7 +42,7 @@ struct matches_directive : unary_parser, Subject> if (x4::has_expectation_failure(ctx)) return false; } - x4::move_to(matched, attr); + x4::write_attribute(attr, matched); return true; } }; diff --git a/include/iris/x4/directive/repeat.hpp b/include/iris/x4/directive/repeat.hpp index c1291b8b4..e1ba8edb5 100644 --- a/include/iris/x4/directive/repeat.hpp +++ b/include/iris/x4/directive/repeat.hpp @@ -90,9 +90,7 @@ struct repeat_directive : proxy_parser, Subjec using attribute_type = traits::default_container::attribute_type>::type; template - static constexpr bool handles_container = - can_hold_v::attribute_type> || - can_hold_v::type, typename parser_traits::attribute_type>; + static constexpr bool accepts_container = writes_into_container; template requires std::is_constructible_v && std::is_constructible_v @@ -106,35 +104,40 @@ struct repeat_directive : proxy_parser, Subjec [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const { - auto& container_attr = list_like_parser::get_container(attr); - list_like_parser::chunk_buffer chunk_buf; + if constexpr (list_like_parser::writes_as_one_element) { + return list_like_parser::parse_as_one_element(*this, first, last, ctx, attr); - It local_it = first; - typename Bounds::value_type i{}; - for (; !bounds_.got_min(i); ++i) { - if (detail::parse_into_container(this->subject, local_it, last, ctx, chunk_buf)) { - // We can't merge here; it will lead to partial status - } else { - return false; + } else { + auto& container_attr = detail::ref_or_init_attribute_for(attr); + list_like_parser::chunk_buffer chunk_buf; + + It local_it = first; + typename Bounds::value_type i{}; + for (; !bounds_.got_min(i); ++i) { + if (detail::parse_into_container(this->subject, local_it, last, ctx, chunk_buf)) { + // We can't merge here; it will lead to partial status + } else { + return false; + } + } + list_like_parser::successful_merge_into(chunk_buf, container_attr); + + first = local_it; + // parse some more up to the maximum specified + for (; !bounds_.got_max(i); ++i) { + if (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + } else { + break; + } } - } - list_like_parser::successful_merge_into(chunk_buf, container_attr); - first = local_it; - // parse some more up to the maximum specified - for (; !bounds_.got_max(i); ++i) { - if (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); + if constexpr (has_context_v) { + return !x4::has_expectation_failure(ctx); } else { - break; + return true; } } - - if constexpr (has_context_v) { - return !x4::has_expectation_failure(ctx); - } else { - return true; - } } template Se, class Context, X4UnusedAttribute UnusedAttr> diff --git a/include/iris/x4/numeric/bool.hpp b/include/iris/x4/numeric/bool.hpp index e1c00524d..7e77d0f0b 100644 --- a/include/iris/x4/numeric/bool.hpp +++ b/include/iris/x4/numeric/bool.hpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include #include @@ -35,11 +35,11 @@ struct bool_policies template Se, X4Attribute Attr, class CaseCompare> [[nodiscard]] static constexpr bool parse_true(It& first, Se const& last, Attr& attr_, CaseCompare const& compare) - noexcept(noexcept(x4::move_to(T(true), attr_))) + noexcept(noexcept(x4::write_attribute(attr_, T(true)))) { using token_def = numeric_token>; if (detail::string_parse(std::basic_string_view{token_def::true_}, first, last, unused_container, compare)) { - x4::move_to(T(true), attr_); + x4::write_attribute(attr_, T(true)); return true; } return false; @@ -48,11 +48,11 @@ struct bool_policies template Se, X4Attribute Attr, class CaseCompare> [[nodiscard]] static constexpr bool parse_false(It& first, Se const& last, Attr& attr_, CaseCompare const& compare) - noexcept(noexcept(x4::move_to(T(false), attr_))) + noexcept(noexcept(x4::write_attribute(attr_, T(false)))) { using token_def = numeric_token>; if (detail::string_parse(std::basic_string_view{token_def::false_}, first, last, unused_container, compare)) { - x4::move_to(T(false), attr_); + x4::write_attribute(attr_, T(false)); return true; } return false; @@ -102,14 +102,14 @@ struct bool_parser : parser> noexcept( std::is_nothrow_default_constructible_v && noexcept(bool_parser::parse(first, last, ctx, std::declval())) && - noexcept(x4::move_to(std::declval(), attr)) + noexcept(x4::write_attribute(attr, std::declval())) ) { static_assert(X4NonUnusedAttribute); T attr_{}; if (bool_parser::parse(first, last, ctx, attr_)) { - x4::move_to(std::move(attr_), attr); + x4::write_attribute(attr, std::move(attr_)); return true; } return false; @@ -166,14 +166,14 @@ struct literal_bool_parser : parser> noexcept( std::is_nothrow_default_constructible_v && noexcept(literal_bool_parser::parse(first, last, ctx, std::declval())) && - noexcept(x4::move_to(std::declval(), attr)) + noexcept(x4::write_attribute(attr, std::declval())) ) { static_assert(X4NonUnusedAttribute); T attr_; if (literal_bool_parser::parse(first, last, ctx, attr_)) { - x4::move_to(std::move(attr_), attr); + x4::write_attribute(attr, std::move(attr_)); return true; } return false; diff --git a/include/iris/x4/numeric/real.hpp b/include/iris/x4/numeric/real.hpp index d1e4e0b9e..f4d38d5c9 100644 --- a/include/iris/x4/numeric/real.hpp +++ b/include/iris/x4/numeric/real.hpp @@ -247,7 +247,7 @@ struct real_parser : parser> noexcept( std::is_nothrow_default_constructible_v && noexcept(real_parser::parse(first, last, ctx, std::declval())) && - noexcept(x4::move_to(std::declval(), attr)) + noexcept(x4::write_attribute(attr, std::declval())) ) { static_assert(X4NonUnusedAttribute); @@ -255,7 +255,7 @@ struct real_parser : parser> // this case is called when Attribute is not T T attr_; if (real_parser::parse(first, last, ctx, attr_)) { - x4::move_to(std::move(attr_), attr); + x4::write_attribute(attr, std::move(attr_)); return true; } return false; diff --git a/include/iris/x4/numeric/utils/extract_int.hpp b/include/iris/x4/numeric/utils/extract_int.hpp index 7c2a9ade8..542794571 100644 --- a/include/iris/x4/numeric/utils/extract_int.hpp +++ b/include/iris/x4/numeric/utils/extract_int.hpp @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include @@ -306,7 +306,7 @@ struct extract_int } if (count + leading_zeros >= MinDigits) { - x4::move_to(std::move(val), attr); + x4::write_attribute(attr, std::move(val)); first = it; return true; } @@ -389,7 +389,7 @@ struct extract_int char_type ch = *it; if (!radix_check::is_valid(ch) || !extractor::call(ch, 0, val)) { if (count == 0) return false; // must have at least one digit - x4::move_to(std::move(val), attr); + x4::write_attribute(attr, std::move(val)); first = it; return true; } @@ -405,7 +405,7 @@ struct extract_int ++count; } - x4::move_to(std::move(val), attr); + x4::write_attribute(attr, std::move(val)); first = it; return true; } @@ -499,13 +499,13 @@ struct extract_uint noexcept( std::is_nothrow_default_constructible_v && noexcept(extract_uint::call(first, last, std::declval())) && - noexcept(x4::move_to(std::declval(), attr)) + noexcept(x4::write_attribute(attr, std::declval())) ) { // this case is called when Attribute is not T T tmp_attr; // default initialize if (extract_uint::call(first, last, tmp_attr)) { - x4::move_to(std::move(tmp_attr), attr); + x4::write_attribute(attr, std::move(tmp_attr)); return true; } return false; @@ -558,13 +558,13 @@ struct extract_int noexcept( std::is_nothrow_default_constructible_v && noexcept(extract_int::call(first, last, std::declval())) && - noexcept(x4::move_to(std::declval(), attr)) + noexcept(x4::write_attribute(attr, std::declval())) ) { // this case is called when Attribute is not T T tmp_attr; // default initialize if (extract_int::call(first, last, tmp_attr)) { - x4::move_to(std::move(tmp_attr), attr); + x4::write_attribute(attr, std::move(tmp_attr)); return true; } return false; diff --git a/include/iris/x4/numeric/utils/extract_real.hpp b/include/iris/x4/numeric/utils/extract_real.hpp index ebabb29b5..e034e32f7 100644 --- a/include/iris/x4/numeric/utils/extract_real.hpp +++ b/include/iris/x4/numeric/utils/extract_real.hpp @@ -12,7 +12,7 @@ =============================================================================*/ #include -#include +#include #include @@ -154,7 +154,7 @@ struct extract_real Policy::parse_inf(first, last, n) ) { // If we got a negative sign, negate the number - x4::move_to(extension::negate(neg, n), attr); + x4::write_attribute(attr, extension::negate(neg, n)); return true; // got a NaN or Inf, return early } @@ -245,7 +245,7 @@ struct extract_real } // If we got a negative sign, negate the number - x4::move_to(extension::negate(neg, n), attr); + x4::write_attribute(attr, extension::negate(neg, n)); // Success!!! return true; diff --git a/include/iris/x4/operator/alternative.hpp b/include/iris/x4/operator/alternative.hpp index f80c31f86..da798cdf8 100644 --- a/include/iris/x4/operator/alternative.hpp +++ b/include/iris/x4/operator/alternative.hpp @@ -34,25 +34,36 @@ struct alternative; namespace detail { +template +struct alternative_wrapped_entry {}; + template -struct to_alternative_attribute_list -{ - using type = type_list; -}; +struct alternative_entry {}; -template<> -struct to_alternative_attribute_list -{ - using type = type_list<>; -}; +template + requires is_recursive_wrapper_v +struct alternative_entry + : alternative_wrapped_entry, T> +{}; template -struct to_alternative_attribute_list> -{ - using type = type_list; -}; +struct alternative_entries + : alternative_entry... +{}; -// ------------------------------------------------------------- +template +Wrapped alternative_wrapped_form(alternative_wrapped_entry, Wrapped>*); + +template +T alternative_wrapped_form(...); + +template +struct unique_alternative_list; + +template +struct unique_alternative_list> + : unique_type_list(static_cast*>(nullptr)))...>> +{}; template struct canonicalize_alternative_attribute; @@ -84,10 +95,10 @@ template struct alternative_layout { using concated_attrs = concat_type_list< - typename to_alternative_attribute_list::attribute_type>::type, - typename to_alternative_attribute_list::attribute_type>::type... + attribute_candidates_t, + attribute_candidates_t... >::type; - using unique_attrs = unique_type_list::type; + using unique_attrs = unique_alternative_list::type>::type; using attribute_type = canonicalize_alternative_attribute::type; // All branches share one attribute type and one slot count; the @@ -112,6 +123,18 @@ struct get_sequence_size> static constexpr std::size_t value = alternative_layout::sequence_size; }; +template +struct attribute_candidates> +{ + using type = alternative_layout::unique_attrs; +}; + +template +struct may_leave_attribute_unwritten> : std::bool_constant< + has_attribute_v> && + ((!has_attribute_v || may_leave_attribute_unwritten_v) || ...) +> {}; + } // detail template diff --git a/include/iris/x4/operator/delimited_list.hpp b/include/iris/x4/operator/delimited_list.hpp index ab091facd..a027c8369 100644 --- a/include/iris/x4/operator/delimited_list.hpp +++ b/include/iris/x4/operator/delimited_list.hpp @@ -28,7 +28,7 @@ struct delimited_list : binary_parser, Left, Right> using attribute_type = traits::default_container::attribute_type>::type; template - static constexpr bool handles_container = WritesIntoContainer; + static constexpr bool accepts_container = writes_into_container; using binary_parser::binary_parser; @@ -36,29 +36,34 @@ struct delimited_list : binary_parser, Left, Right> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const { - auto& container_attr = list_like_parser::get_container(attr); - list_like_parser::chunk_buffer chunk_buf; + if constexpr (list_like_parser::writes_as_one_element) { + return list_like_parser::parse_as_one_element(*this, first, last, ctx, attr); - // In order to succeed, we need to match at least one element - if (detail::parse_into_container(this->left, first, last, ctx, chunk_buf)) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); } else { - return false; - } - - It last_parse_it = first; - while ( - this->right.parse(last_parse_it, last, ctx, unused) && - detail::parse_into_container(this->left, last_parse_it, last, ctx, chunk_buf) - ) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); - first = last_parse_it; - } - - if constexpr (has_context_v) { - return !x4::has_expectation_failure(ctx); - } else { - return true; + auto& container_attr = detail::ref_or_init_attribute_for(attr); + list_like_parser::chunk_buffer chunk_buf; + + // In order to succeed, we need to match at least one element + if (detail::parse_into_container(this->left, first, last, ctx, chunk_buf)) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + } else { + return false; + } + + It last_parse_it = first; + while ( + this->right.parse(last_parse_it, last, ctx, unused) && + detail::parse_into_container(this->left, last_parse_it, last, ctx, chunk_buf) + ) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + first = last_parse_it; + } + + if constexpr (has_context_v) { + return !x4::has_expectation_failure(ctx); + } else { + return true; + } } } diff --git a/include/iris/x4/operator/difference.hpp b/include/iris/x4/operator/difference.hpp index be2734138..366115cc3 100644 --- a/include/iris/x4/operator/difference.hpp +++ b/include/iris/x4/operator/difference.hpp @@ -25,6 +25,9 @@ struct difference : binary_parser, Left, Right> { using attribute_type = parser_traits::attribute_type; + template + static constexpr bool accepts_container = parser_traits::template accepts_container; + using binary_parser::binary_parser; template Se, class Context, X4Attribute Attr> @@ -58,6 +61,16 @@ struct difference : binary_parser, Left, Right> } }; +namespace detail { + +template +struct may_leave_attribute_unwritten> : may_leave_attribute_unwritten {}; + +template +struct attribute_candidates> : attribute_candidates {}; + +} // detail + template [[nodiscard]] constexpr difference, as_parser_plain_t> operator-(Left&& left, Right&& right) diff --git a/include/iris/x4/operator/kleene.hpp b/include/iris/x4/operator/kleene.hpp index 702d0bd87..6f82c370a 100644 --- a/include/iris/x4/operator/kleene.hpp +++ b/include/iris/x4/operator/kleene.hpp @@ -29,7 +29,7 @@ struct kleene : unary_parser, Subject> using attribute_type = traits::default_container::attribute_type>::type; template - static constexpr bool handles_container = WritesIntoContainer; + static constexpr bool accepts_container = writes_into_container; using unary_parser::unary_parser; @@ -37,17 +37,22 @@ struct kleene : unary_parser, Subject> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const { - auto& container_attr = list_like_parser::get_container(attr); - list_like_parser::chunk_buffer chunk_buf; + if constexpr (list_like_parser::writes_as_one_element) { + return list_like_parser::parse_as_one_element(*this, first, last, ctx, attr); - while (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); - } - - if constexpr (has_context_v) { - return !x4::has_expectation_failure(ctx); } else { - return true; + auto& container_attr = detail::ref_or_init_attribute_for(attr); + list_like_parser::chunk_buffer chunk_buf; + + while (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + } + + if constexpr (has_context_v) { + return !x4::has_expectation_failure(ctx); + } else { + return true; + } } } diff --git a/include/iris/x4/operator/optional.hpp b/include/iris/x4/operator/optional.hpp index 6289c382f..b6d98a907 100644 --- a/include/iris/x4/operator/optional.hpp +++ b/include/iris/x4/operator/optional.hpp @@ -34,7 +34,7 @@ struct optional : unary_parser, Subject> using attribute_type = build_optional::attribute_type>::type; template - static constexpr bool handles_container = WritesIntoContainer; + static constexpr bool accepts_container = writes_into_container; using unary_parser::unary_parser; @@ -68,7 +68,7 @@ struct optional : unary_parser, Subject> { if (this->subject.parse( first, last, ctx, - detail::prepare_attribute::attribute_type>(attr) + detail::prepare_attribute_for(attr) )) { return true; } @@ -91,20 +91,16 @@ struct optional : unary_parser, Subject> { // Same logic as in `x4::alternative` - if (traits::is_empty(attr)) { + if (std::ranges::empty(attr)) { if (detail::parse_into_container(this->subject, first, last, ctx, attr)) { return true; } - traits::clear(attr); + iris::container::clear(attr); } else { - unwrap_container_appender_t buffer; + Attr buffer; if (detail::parse_into_container(this->subject, first, last, ctx, buffer)) { - traits::append( - attr, - std::make_move_iterator(traits::begin(buffer)), - std::make_move_iterator(traits::end(buffer)) - ); + iris::container::append_range(attr, buffer | std::views::as_rvalue); return true; } } diff --git a/include/iris/x4/operator/plus.hpp b/include/iris/x4/operator/plus.hpp index 0bb3a02ae..069725b9f 100644 --- a/include/iris/x4/operator/plus.hpp +++ b/include/iris/x4/operator/plus.hpp @@ -29,7 +29,7 @@ struct plus : unary_parser, Subject> using attribute_type = traits::default_container::attribute_type>::type; template - static constexpr bool handles_container = WritesIntoContainer; + static constexpr bool accepts_container = writes_into_container; using unary_parser::unary_parser; @@ -37,23 +37,28 @@ struct plus : unary_parser, Subject> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const { - auto& container_attr = list_like_parser::get_container(attr); - list_like_parser::chunk_buffer chunk_buf; + if constexpr (list_like_parser::writes_as_one_element) { + return list_like_parser::parse_as_one_element(*this, first, last, ctx, attr); - if (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); } else { - return false; - } - - while (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { - list_like_parser::successful_merge_into(chunk_buf, container_attr); - } - - if constexpr (has_context_v) { - return !x4::has_expectation_failure(ctx); - } else { - return true; + auto& container_attr = detail::ref_or_init_attribute_for(attr); + list_like_parser::chunk_buffer chunk_buf; + + if (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + } else { + return false; + } + + while (detail::parse_into_container(this->subject, first, last, ctx, chunk_buf)) { + list_like_parser::successful_merge_into(chunk_buf, container_attr); + } + + if constexpr (has_context_v) { + return !x4::has_expectation_failure(ctx); + } else { + return true; + } } } diff --git a/include/iris/x4/operator/sequence.hpp b/include/iris/x4/operator/sequence.hpp index dc50a2d13..f9573d022 100644 --- a/include/iris/x4/operator/sequence.hpp +++ b/include/iris/x4/operator/sequence.hpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -33,7 +32,7 @@ #include #include -#include +#include // IWYU pragma: keep namespace iris::x4 { @@ -42,61 +41,18 @@ struct sequence; namespace detail { -template -struct container_can_hold_element : std::is_same -{}; - -template - requires - (!std::same_as) && - (!traits::X4Container) && - requires(Container& c, Elem&& elem) { - traits::push_back(c, std::move(elem)); - } -struct container_can_hold_element - : std::true_type -{}; - -template - requires - (!std::same_as) && - traits::X4Container && - requires(Container& c, ContainerElem&& container_elem) { - x4::move_to( - std::make_move_iterator(traits::begin(container_elem)), - std::make_move_iterator(traits::end(container_elem)), - c - ); - } -struct container_can_hold_element - : std::true_type -{}; - -template -struct container_can_hold_sequence : container_can_hold_element -{}; - -template -struct container_can_hold_sequence> - // this should not delegate to `container_can_hold_sequence`; we don't want recursive expansion - : std::conjunction...> -{}; - template struct get_sequence_size> { static constexpr std::size_t value = sequence_layout::total_sequence_size; }; +// A sequence accepts a container when every element of it can write into +// the container; the sequence as a whole then writes nothing of its own. template -struct get_handles_container, Container> +struct get_accepts_container, Container> { - static constexpr bool value = - (parser_traits::template handles_container && ...) || - container_can_hold_sequence< - Container, - typename parser_traits>::attribute_type - >::value; + static constexpr bool value = ((!has_attribute_v || writes_into_container) && ...); }; // ------------------------------------------------------------- diff --git a/include/iris/x4/parse.hpp b/include/iris/x4/parse.hpp index 6922fa268..6b59dcb87 100644 --- a/include/iris/x4/parse.hpp +++ b/include/iris/x4/parse.hpp @@ -206,7 +206,7 @@ struct parse_fn expect_failure, x4::make_context(skipper_kind) ), - detail::prepare_attribute>(root_attr) + detail::prepare_attribute_for>(root_attr) ); if (ok) reset_guard.commit(); @@ -241,7 +241,7 @@ struct parse_fn res.expect_failure, x4::make_context(skipper_kind) ), - detail::prepare_attribute>(root_attr) + detail::prepare_attribute_for>(root_attr) ); if (res.ok) reset_guard.commit(); @@ -273,7 +273,7 @@ struct parse_fn attribute_reset_guard reset_guard{root_attr}; bool ok = as_parser(std::forward(p)).parse( - first, last, ctx, detail::prepare_attribute>(root_attr) + first, last, ctx, detail::prepare_attribute_for>(root_attr) ); if (ok && flag == root_skipper_flag::do_post_skip) { x4::skip_over(first, last, ctx); @@ -312,7 +312,7 @@ struct parse_fn attribute_reset_guard reset_guard{root_attr}; res.ok = as_parser(std::forward(p)).parse( first, last, ctx, - detail::prepare_attribute>(root_attr) + detail::prepare_attribute_for>(root_attr) ); if (res.ok && flag == root_skipper_flag::do_post_skip) { x4::skip_over(first, last, ctx); @@ -341,7 +341,7 @@ struct parse_fn expect_failure, x4::make_context(skipper_kind) ), - detail::prepare_attribute>(root_attr) + detail::prepare_attribute_for>(root_attr) ); if (ok) reset_guard.commit(); @@ -367,7 +367,7 @@ struct parse_fn res.expect_failure, x4::make_context(skipper_kind) ), - detail::prepare_attribute>(root_attr) + detail::prepare_attribute_for>(root_attr) ); if (res.ok) reset_guard.commit(); @@ -391,7 +391,7 @@ struct parse_fn attribute_reset_guard reset_guard{root_attr}; bool ok = as_parser(std::forward(p)).parse( - first, last, ctx, detail::prepare_attribute>(root_attr) + first, last, ctx, detail::prepare_attribute_for>(root_attr) ); if (ok && flag == root_skipper_flag::do_post_skip) { x4::skip_over(first, last, ctx); @@ -421,7 +421,7 @@ struct parse_fn attribute_reset_guard reset_guard{root_attr}; res.ok = as_parser(std::forward(p)).parse( - first, last, ctx, detail::prepare_attribute>(root_attr) + first, last, ctx, detail::prepare_attribute_for>(root_attr) ); if (res.ok && flag == root_skipper_flag::do_post_skip) { x4::skip_over(first, last, ctx); diff --git a/include/iris/x4/parse_debug.hpp b/include/iris/x4/parse_debug.hpp index a8bd8fc64..0848c084b 100644 --- a/include/iris/x4/parse_debug.hpp +++ b/include/iris/x4/parse_debug.hpp @@ -14,6 +14,18 @@ namespace iris::x4 { namespace detail { +// Helper for making `std::println` instantiated only when this template is instantiated +template +void print_debug_output(OStream const& trace_out, OStream const& error_out, bool const failed) +{ + if (!trace_out.str().empty()) { + std::println("{}", trace_out.str()); + } + if (failed && !error_out.str().empty()) { + std::println("{}", error_out.str()); + } +} + struct parse_debug_fn_main { // -------------------------------------------- @@ -32,10 +44,7 @@ struct parse_debug_fn_main }; auto const res = x4::parse(range_, x4::with(error_handler)[std::forward(p)], attr); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); return res; } @@ -52,10 +61,7 @@ struct parse_debug_fn_main }; x4::parse(res, range_, x4::with(error_handler)[std::forward(p)], attr); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); } // -------------------------------------------- @@ -74,10 +80,7 @@ struct parse_debug_fn_main }; auto const res = x4::parse(range_, x4::with(error_handler)[std::forward(p)], s, attr, flag); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); return res; } @@ -94,10 +97,7 @@ struct parse_debug_fn_main }; x4::parse(res, range_, x4::with(error_handler)[std::forward(p)], s, attr, flag); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); } // -------------------------------------------- @@ -115,10 +115,7 @@ struct parse_debug_fn_main }; auto const res = x4::parse(first, last, x4::with(error_handler)[std::forward(p)], attr); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); return res; } @@ -134,10 +131,7 @@ struct parse_debug_fn_main }; x4::parse(res, first, last, x4::with(error_handler)[std::forward(p)], attr); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); } // -------------------------------------------- @@ -155,10 +149,7 @@ struct parse_debug_fn_main }; auto const res = x4::parse(first, last, x4::with(error_handler)[std::forward(p)], s, attr, flag); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); return res; } @@ -174,10 +165,7 @@ struct parse_debug_fn_main }; x4::parse(res, first, last, x4::with(error_handler)[std::forward(p)], s, attr, flag); - if (!trace_out.str().empty()) std::println("{}", trace_out.str()); - if (!res && !error_out.str().empty()) { - std::println("{}", error_out.str()); - } + detail::print_debug_output(trace_out, error_out, !res); } }; // parse_debug_fn diff --git a/include/iris/x4/rule.hpp b/include/iris/x4/rule.hpp index 8dc83cb23..213bd5ce1 100644 --- a/include/iris/x4/rule.hpp +++ b/include/iris/x4/rule.hpp @@ -13,21 +13,21 @@ #include // IWYU pragma: keep -#include +#include +#include +#include +#include #include #include #include #include #include -#include #include #include -#include - #include #include #include @@ -96,7 +96,7 @@ struct rule_impl using rcontext_t = std::remove_cvref_t< decltype(x4::replace_first_context( std::declval(), - std::declval::type&>() + std::declval() )) >; @@ -247,21 +247,15 @@ struct rule_impl Context const& ctx, Exposed& exposed_attr ) { - // Do down-stream transformation, provide attribute for `rhs` parser - using transform = transform_attribute; - using transform_attr = transform::type; - transform_attr rhs_attr = transform::pre(exposed_attr); + static_assert(std::same_as || X4UnusedAttribute); // Creates a place to hold the result of parse_rhs // called inside the following scope. bool parse_ok = false; { - // Debug on destructor, i.e., before any modifications are made to the - // attribute passed to `parse_rhs`. Note: the debug must be done before - // `transform::post`, where some types do some modifications there; - // for instance, if `Exposed` is a recursive variant. - [[maybe_unused]] scoped_tracer> - scoped_tracer{first, last, ctx, rhs_attr, rule_name, &parse_ok}; + // Debug on destructor + [[maybe_unused]] scoped_tracer + scoped_tracer{first, last, ctx, exposed_attr, rule_name, &parse_ok}; // The existence of semantic action inhibits attribute materialization _unless_ it is // explicitly required by the user (primarily via `%=`). @@ -273,13 +267,13 @@ struct rule_impl if constexpr (ForceAttr) { parse_ok = rule_impl::parse_rhs( rhs, first, last, - rule_impl::make_rcontext(ctx, rhs_attr), - rhs_attr + rule_impl::make_rcontext(ctx, exposed_attr), + exposed_attr ); } else { parse_ok = rule_impl::parse_rhs( rhs, first, last, - rule_impl::make_rcontext(ctx, rhs_attr), + rule_impl::make_rcontext(ctx, exposed_attr), unused // <-- omitted attribute ); } @@ -287,16 +281,11 @@ struct rule_impl } else { // RHS has no semantic action parse_ok = rule_impl::parse_rhs( rhs, first, last, - rule_impl::make_rcontext(ctx, rhs_attr), - rhs_attr + rule_impl::make_rcontext(ctx, exposed_attr), + exposed_attr ); } } - - if (parse_ok) { - // Integrate the results back into the original attribute value, if appropriate - transform::post(exposed_attr, std::forward(rhs_attr)); - } return parse_ok; } }; @@ -329,10 +318,18 @@ struct rule_definition : parser - ::template call_rule_definition( - this->rhs_, this->name, first, last, ctx, attr - ); + using impl = rule_impl; + + if constexpr (std::same_as || X4UnusedAttribute) { + return impl::template call_rule_definition(this->rhs_, this->name, first, last, ctx, attr); + + } else { + // Used directly as a parser with another attribute: parse into the attribute of the rule and move it on success + attribute_type rule_attr{}; + if (!impl::template call_rule_definition(this->rhs_, this->name, first, last, ctx, rule_attr)) return false; + x4::write_attribute(attr, std::move(rule_attr)); + return true; + } } private: @@ -342,51 +339,6 @@ struct rule_definition : parser -struct narrowing_checker -{ - using Dest = Exposed[]; - - // emulate `Exposed x[] = {std::forward(t)};` - template - static void operator()(T&&) - requires requires(T&& t) { { Dest{std::forward(t)} }; }; -}; - - -template -concept RuleAttrConvertible = - X4Attribute && - std::is_assignable_v>&, RuleAttr>; - -template -concept RuleAttrConvertibleWithoutNarrowing = - RuleAttrConvertible && - requires { - narrowing_checker< - unwrap_container_appender_t> - >::operator()(std::declval()); - }; - -// Resolves "The Spirit X3 rule problem" in Boost.Parser's documentation -// https://www.boost.org/doc/libs/1_89_0/doc/html/boost_parser/this_library_s_relationship_to_boost_spirit.html#boost_parser.this_library_s_relationship_to_boost_spirit.the_spirit_x3_rule_problem -// https://github.com/boostorg/spirit_x4/issues/38 -template -concept RuleAttrTransformable = - X4Attribute> && - X4Attribute && - std::default_initializable && - RuleAttrConvertible && - RuleAttrConvertibleWithoutNarrowing< - unwrap_container_appender_t>, - RuleAttr - >; - -template -concept RuleAttrCompatible = - std::same_as, RuleAttr> || - RuleAttrTransformable; - } // detail template @@ -396,10 +348,12 @@ struct rule : parser> // Do NOT add `static_assert`s or other constructs that cause eager // instantiation of `RuleAttr` within this class body. + static_assert(!std::is_const_v); + using id = RuleID; using attribute_type = RuleAttr; - static constexpr bool has_attribute = !std::is_same_v, unused_type>; + static constexpr bool has_attribute = !std::is_same_v; static constexpr bool force_attribute = ForceAttr; std::string_view name = "unnamed_rule"; @@ -420,9 +374,7 @@ struct rule : parser> // Primary overload template Se, class Context, X4Attribute Exposed> - requires - (!std::same_as, unused_type>) && - detail::RuleAttrCompatible + requires (!std::same_as, unused_type>) [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Exposed& exposed_attr) const { @@ -443,42 +395,36 @@ struct rule : parser> using detail::parse_rule; // ADL if constexpr (std::same_as, RuleAttr>) { + if constexpr (traits::X4Container) { + if (!std::ranges::empty(exposed_attr)) { + // The container holds the preceding results, which the attribute of the rule + // is kept apart from: parse into a new attribute and append it on success + RuleAttr rule_attr{}; + if (!static_cast(parse_rule(detail::rule_id{}, first, last, rule_agnostic_ctx, rule_attr))) { // NOLINT(bugprone-non-zero-enum-to-bool-conversion) + return false; + } + planner::pass_declared_attribute(exposed_attr, std::move(rule_attr)); + return true; + } + } return static_cast(parse_rule(detail::rule_id{}, first, last, rule_agnostic_ctx, exposed_attr)); // NOLINT(bugprone-non-zero-enum-to-bool-conversion) - } else { - static_assert(detail::RuleAttrTransformable); + } else if constexpr (detail::holds_as_single_element, RuleAttr>) { + return this->parse(first, last, ctx, alloy::get<0>(exposed_attr)); - // TODO: specialize `container_appender` case, do not create temporary + } else { + static_assert(X4StrictlyWritable&, unwrap_recursive_t&&>); + static_assert(!detail::dangles, unwrap_recursive_t&&>); - RuleAttr rule_attr; + RuleAttr rule_attr{}; if (!static_cast(parse_rule(detail::rule_id{}, first, last, rule_agnostic_ctx, rule_attr))) { // NOLINT(bugprone-non-zero-enum-to-bool-conversion) return false; } - - if constexpr (is_ttp_specialization_of_v, container_appender>) { - traits::append( - exposed_attr.container, - std::make_move_iterator(traits::begin(rule_attr)), - std::make_move_iterator(traits::end(rule_attr)) - ); - } else { - static_assert(std::is_assignable_v); - exposed_attr = std::move(rule_attr); - } + planner::pass_declared_attribute(exposed_attr, iris::unwrap_recursive(std::move(rule_attr))); return true; } } - template Se, class Context, X4Attribute Exposed> - requires - (!std::same_as, unused_type>) && - (!detail::RuleAttrCompatible) && - detail::RuleAttrConvertible && - (!detail::RuleAttrConvertibleWithoutNarrowing) - [[nodiscard]] constexpr bool - parse(It&, Se const&, Context const&, Exposed&) const = delete; // Rule attribute needs narrowing conversion - - template Se, class Context> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, unused_type const&) const @@ -565,9 +511,8 @@ struct rule : parser> static_assert(X4Attribute); if constexpr (X4NonUnusedAttribute) { static_assert(X4ValueAttribute); - static_assert(!std::is_const_v, "Rule attribute cannot be const qualified"); } - static_assert(!std::is_same_v, unused_container_type>, "`rule` with `unused_container_type` is not supported"); + static_assert(!std::is_same_v, "`rule` with `unused_container_type` is not supported"); } }; diff --git a/include/iris/x4/string/detail/string_parse.hpp b/include/iris/x4/string/detail/string_parse.hpp index ed02f408a..d84558627 100644 --- a/include/iris/x4/string/detail/string_parse.hpp +++ b/include/iris/x4/string/detail/string_parse.hpp @@ -12,7 +12,7 @@ #include -#include +#include #include #include @@ -46,7 +46,7 @@ string_parse( } } - x4::move_to(first, it, attr); + x4::write_attribute(attr, std::ranges::subrange(first, it)); first = it; return true; } @@ -90,7 +90,7 @@ string_parse( return false; } } - x4::move_to(first, it, attr); + x4::write_attribute(attr, std::ranges::subrange(first, it)); first = it; return true; } diff --git a/include/iris/x4/string/literal_string.hpp b/include/iris/x4/string/literal_string.hpp index 22f92dc8b..293c9fa82 100644 --- a/include/iris/x4/string/literal_string.hpp +++ b/include/iris/x4/string/literal_string.hpp @@ -55,16 +55,19 @@ struct literal_string : parser> template Se, class Context, X4Attribute ExposedAttr> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, ExposedAttr& exposed_attr) const - noexcept( - std::is_nothrow_copy_assignable_v && - noexcept(x4::skip_over(first, last, ctx)) && - noexcept(detail::string_parse(this->str_, first, last, x4::assume_container(exposed_attr), x4::get_case_compare(ctx))) - ) { static_assert(std::same_as, char_type>, "Mixing incompatible char types is not allowed"); + auto it = first; x4::skip_over(it, last, ctx); - bool const ok = detail::string_parse(this->str_, it, last, x4::assume_container(exposed_attr), x4::get_case_compare(ctx)); + + bool ok; + if constexpr (has_attribute) { + ok = detail::string_parse(this->str_, it, last, x4::assume_container(exposed_attr), x4::get_case_compare(ctx)); + } else { + (void)exposed_attr; + ok = detail::string_parse(this->str_, it, last, x4::unused_container, x4::get_case_compare(ctx)); + } if (ok) first = it; return ok; } diff --git a/include/iris/x4/symbols.hpp b/include/iris/x4/symbols.hpp index 3fdc92fbd..4450c5b78 100644 --- a/include/iris/x4/symbols.hpp +++ b/include/iris/x4/symbols.hpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include @@ -240,17 +240,12 @@ struct symbols_parser_impl : parser Se, class Context, X4Attribute Attr> [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr) const - noexcept( - std::is_nothrow_copy_assignable_v && - noexcept(x4::skip_over(first, last, ctx)) && - noexcept(x4::move_to(std::declval(), attr)) - ) { auto it = first; x4::skip_over(it, last, ctx); if (value_type const* val_ptr = lookup->find(it, last, x4::get_case_compare(ctx))) { - x4::move_to(*val_ptr, attr); + x4::write_attribute(attr, *val_ptr); first = it; return true; } diff --git a/include/iris/x4/traits/attribute_traits.hpp b/include/iris/x4/traits/attribute_traits.hpp index 2dda59fee..3128460ad 100644 --- a/include/iris/x4/traits/attribute_traits.hpp +++ b/include/iris/x4/traits/attribute_traits.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include // IWYU pragma: keep @@ -23,8 +24,7 @@ #include #include -#include -#include +#include // IWYU pragma: keep namespace iris::x4::traits { @@ -38,6 +38,15 @@ concept clearable_for = requires(ExposedAttr& attr) { attribute_traits::template clear(attr); }; +// The part that `ParserAttr` is written into is prepared in place only if it is +// not plain, or of the type of the value. Otherwise, the whole attribute is reset, +// and a new plain part is directly constructed from the value when it is written +// (i.e., never default-constructed-then-assigned). +template +concept creatable_in_place_for = + !CategorizedAttr, plain_tag> || + std::same_as, unwrap_recursive_t>; + template concept proper_attribute_for = X4ValueAttribute && @@ -59,9 +68,9 @@ struct attribute_traits static_assert(CategorizedAttr); static constexpr void reset(ExposedAttr& attr) - noexcept(noexcept(attr = ExposedAttr{})) + noexcept(noexcept(attr = ExposedAttr())) { - attr = ExposedAttr{}; + attr = ExposedAttr(); } template @@ -69,9 +78,10 @@ struct attribute_traits std::default_initializable && weakly_assignable_from static constexpr ExposedAttr& clear(ExposedAttr& attr) + noexcept(noexcept(attr = ParserAttr())) { static_assert(detail::proper_attribute_for); - attr = ParserAttr{}; + attr = ParserAttr(); return attr; } @@ -81,10 +91,11 @@ struct attribute_traits (!weakly_assignable_from) && std::constructible_from static constexpr ExposedAttr& clear(ExposedAttr& attr) + noexcept(noexcept(attr = ExposedAttr(ParserAttr()))) { static_assert(std::default_initializable); static_assert(detail::proper_attribute_for); - attr = ExposedAttr{ParserAttr{}}; + attr = ExposedAttr(ParserAttr()); return attr; } }; @@ -130,6 +141,7 @@ struct attribute_traits template requires (!CategorizedAttr) && + detail::creatable_in_place_for && detail::clearable_for static constexpr decltype(auto) clear(OptionalT& opt) { @@ -181,41 +193,34 @@ struct attribute_traits } } - template - using tag_type = unwrap_recursive_t::type>; - + // Clears the alternative that `ParserAttr` is written into template requires (!std::same_as) && - requires(VariantT& var) { var.template emplace>(); } - static constexpr tag_type& clear(VariantT& var) + x4::detail::variant_has_alternative_for_v && + detail::creatable_in_place_for, VariantT>, ParserAttr> && + requires(VariantT& var) { var.template emplace>(); } + static constexpr auto& clear(VariantT& var) { - if (auto* existing_alt = iris::get_if>(&var)) { - attribute_traits>::template clear>(*existing_alt); + constexpr std::size_t index = x4::detail::variant_alternative_for_v; + using alternative_type = variant_alternative_t; + + if (auto* existing_alt = iris::get_if(&var)) { + attribute_traits::template clear(*existing_alt); return *existing_alt; } else { - return var.template emplace>(); + return var.template emplace(); } } + // The default state is the first alternative; the alternative held is not kept, since a parser + // yielding the variant itself may succeed without writing into it template requires std::same_as static constexpr VariantT& clear(VariantT& var) { - assert(!var.valueless_by_exception()); - - // Reuse the existing alternative instance. - // We don't call `visit(...)` here, since it increases the compilation time by few hundred msec - [&](std::index_sequence) { - (void)( - ( - var.index() == Is - ? (attribute_traits>::reset(*iris::get_if(&var)), true) - : false - ) || ... - ); - }(std::make_index_sequence>{}); + attribute_traits::reset(var); return var; } }; @@ -224,15 +229,15 @@ template ContainerT> struct attribute_traits { static constexpr void reset(ContainerT& container) - noexcept(noexcept(traits::clear(container))) + noexcept(noexcept(iris::container::clear(container))) { - traits::clear(container); + iris::container::clear(container); } template static constexpr ContainerT& clear(ContainerT& container) { - traits::clear(container); + iris::container::clear(container); return container; } }; @@ -272,13 +277,13 @@ struct attribute_traits template requires - (!std::same_as) && + (!std::same_as, TupleLikeT>) && (alloy::tuple_size_v == 1) && detail::clearable_for, ParserAttr> static constexpr decltype(auto) clear(TupleLikeT& tup) { - // A single-element tuple-like is transparent, as in `move_to`: the - // branch parses into the element. + // Unless the branch yields the single-element tuple-like itself, parse into its element + // (consistent with `write_rank`). return attribute_traits>::template clear( alloy::get<0>(tup) ); @@ -289,26 +294,25 @@ struct attribute_traits namespace iris::x4::detail { -template -[[nodiscard]] constexpr unused_type const& prepare_attribute(ExposedAttr& exposed_attr) +// Prepares `exposed_attr` for `Parser` in its default state: the part the attribute type of `Parser` +// is written into, or the whole attribute for a parser that may succeed without writing it. +template +[[nodiscard]] constexpr decltype(auto) +prepare_attribute_for(ExposedAttr& exposed_attr) { - traits::attribute_traits::reset(exposed_attr); - return unused; -} + using parser_attr = parser_traits::attribute_type; -template - requires traits::detail::clearable_for -[[nodiscard]] constexpr decltype(auto) prepare_attribute(ExposedAttr& exposed_attr IRIS_LIFETIMEBOUND) -{ - return traits::attribute_traits::template clear(exposed_attr); -} + if constexpr (X4UnusedAttribute) { + traits::attribute_traits::reset(exposed_attr); + return (unused); -template - requires (!traits::detail::clearable_for) -[[nodiscard]] constexpr ExposedAttr& prepare_attribute(ExposedAttr& exposed_attr IRIS_LIFETIMEBOUND) -{ - traits::attribute_traits::reset(exposed_attr); - return exposed_attr; + } else if constexpr (!may_leave_attribute_unwritten_v && traits::detail::clearable_for) { + return traits::attribute_traits::template clear(exposed_attr); + + } else { + traits::attribute_traits::reset(exposed_attr); + return (exposed_attr); + } } template diff --git a/include/iris/x4/traits/container_traits.hpp b/include/iris/x4/traits/container_traits.hpp index bf17a1233..a0a2b5813 100644 --- a/include/iris/x4/traits/container_traits.hpp +++ b/include/iris/x4/traits/container_traits.hpp @@ -11,473 +11,28 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) =============================================================================*/ -#include +#include // IWYU pragma: keep + #include -#include #include +#include + #include -#include -#include -#include #include #include -#include - -namespace iris::x4 { - -template -struct container_appender; - -} // iris::x4 +#include namespace iris::x4::traits { -// Customization point -template -struct is_associative : std::false_type -{ - static_assert(!std::is_reference_v); - static_assert(!std::is_const_v); -}; - template - requires requires { - typename T::key_type; - } -struct is_associative : std::true_type -{ - static_assert(!std::is_reference_v); - static_assert(!std::is_const_v); -}; - -template -constexpr bool is_associative_v = is_associative::value; - - -namespace detail { - -template -struct remove_value_const -{ - using type = T; -}; - -template -struct remove_value_const : remove_value_const -{}; - -template -struct remove_value_const> -{ - using type = std::pair< - typename remove_value_const::type, - typename remove_value_const::type - >; -}; - -} // detail - -// Customization point -template -struct container_value {}; - -template - requires requires { typename Container::value_type; } -struct container_value - : detail::remove_value_const -{}; - -template -struct container_value : container_value {}; - -template<> -struct container_value -{ - using type = unused_type; +concept X4Container = requires { + requires std::default_initializable>; + requires iris::container::growable_array>; }; - -// Customization point -template - requires requires(Container& c) { - std::ranges::begin(c); - } -struct container_iterator -{ - using type = decltype(std::ranges::begin(std::declval())); -}; - -template -using container_iterator_t = typename container_iterator::type; - - -// Customization point -template - requires requires(Container& c) { - std::ranges::end(c); - } -struct container_sentinel -{ - using type = decltype(std::ranges::end(std::declval())); -}; - -template -using container_sentinel_t = typename container_sentinel::type; - - -// Customization point -template -struct push_back_container; // not defined - -namespace detail { - -struct push_back_fn -{ - template - static constexpr void - operator()(Container const&, T const&) = delete; - - template - static constexpr void operator()(Container&, unused_type const&) noexcept - { - static_assert(!std::same_as, unused_type>); - static_assert(!std::same_as, unused_container_type>); - } - - template - requires requires(Container& c) { - c.push_back(std::declval()); - } - static constexpr void operator()(Container& c, T&& val) - noexcept(noexcept(c.push_back(std::declval()))) - { - c.push_back(std::forward(val)); - } - - template - requires (!requires(Container& c) { - c.push_back(std::declval()); - }) && requires(Container& c) { - c.insert(std::ranges::end(c), std::declval()); - } - static constexpr void operator()(Container& c, T&& val) - noexcept(noexcept(c.insert(std::ranges::end(c), std::declval()))) - { - c.insert(std::ranges::end(c), std::forward(val)); - } - - template - requires requires(Container& c) { - push_back_container::call(c, std::declval()); - } - static constexpr void operator()(Container& c, T&& val) - noexcept(noexcept(push_back_container::call(c, std::forward(val)))) - { - static_assert(!std::same_as, unused_type>); - static_assert(!std::same_as, unused_container_type>); - push_back_container::call(c, std::forward(val)); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::push_back_fn push_back{}; - -} // cpos - - -// Customization point -template -struct append_container; // not defined - -namespace detail { - -struct append_fn -{ - template Se> - static constexpr bool is_specialized = requires(Container& c, It first, Se last) { - append_container::call(c, first, last); - }; - - template Se> - requires - (!is_specialized) && - is_associative_v && - requires(Container& c, It first, Se last) { - c.insert(first, last); - } - static constexpr void operator()(Container& c, It first, Se last) - noexcept(noexcept(c.insert(first, last))) - { - // appending incompatible type into a container can result in unexpected behavior - // e.g. appending `int` into `vector>` compiles, but gets resolved into `vector::vector(size_t)` - static_assert(std::constructible_from::type, std::iter_value_t>); - c.insert(first, last); - } - - template Se> - requires - (!is_specialized) && - (!is_associative_v) && - requires(Container& c, It first, Se last) { - c.insert(std::ranges::end(c), first, last); - } - static constexpr void operator()(Container& c, It first, Se last) - noexcept(noexcept(c.insert(std::ranges::end(c), first, last))) - { - // appending incompatible type into a container can result in unexpected behavior - // e.g. appending `int` into `vector>` compiles, but gets resolved into `vector::vector(size_t)` - static_assert(std::constructible_from::type, std::iter_value_t>); - c.insert(std::ranges::end(c), first, last); - } - - template Se> - requires is_specialized - static constexpr void operator()(Container& c, It first, Se last) - noexcept(noexcept(append_container::call(c, first, last))) - { - static_assert(!std::same_as, unused_type>); - static_assert(!std::same_as, unused_container_type>); - - // appending incompatible type into a container can result in unexpected behavior - // e.g. appending `int` into `vector>` compiles, but gets resolved into `vector::vector(size_t)` - static_assert(std::constructible_from::type, std::iter_value_t>); - append_container::call(c, first, last); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::append_fn append{}; - -} // cpos - - -// Customization point -template -struct clear_container; // not defined - -namespace detail { - -struct clear_fn -{ - template - static constexpr void - operator()(Container const&) = delete; - - template - requires requires(Container& c) { - c.clear(); - } - static constexpr void - operator()(Container& c) noexcept(noexcept(c.clear())) - { - static_assert(!std::same_as); - static_assert(!std::same_as); - static_assert(!std::is_const_v); - c.clear(); - } - - template - requires requires(Container& c) { - { clear_container::call(c) }; - } - static constexpr void - operator()(Container& c) - noexcept(noexcept(clear_container::call(c))) - { - static_assert(!std::same_as); - static_assert(!std::same_as); - static_assert(!std::is_const_v); - return clear_container::call(c); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::clear_fn clear{}; - -} // cpos - - -// Customization point -template -struct is_empty_container; // not defined - -namespace detail { - -struct is_empty_fn -{ - template - requires requires(Container const& c) { - std::ranges::empty(c); - } - [[nodiscard]] static constexpr bool - operator()(Container const& c) noexcept - { - static_assert(!std::same_as); - static_assert(!std::same_as); - return std::ranges::empty(c); - } - - template - requires requires(Container const& c) { - { is_empty_container::call(c) } -> std::same_as; - } - [[nodiscard]] static constexpr bool - operator()(Container const& c) - noexcept(noexcept(is_empty_container::call(c))) - { - static_assert(!std::same_as); - static_assert(!std::same_as); - return is_empty_container::call(c); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::is_empty_fn is_empty{}; - -} // cpos - - -// Customization point -template -struct begin_container; // not defined - -namespace detail { - -struct begin_fn -{ - template - requires requires(Container& c) { - std::ranges::begin(c); - } - [[nodiscard]] static constexpr container_iterator_t - operator()(Container& c) - noexcept(noexcept(std::ranges::begin(c))) - { - static_assert(std::same_as>); - return std::ranges::begin(c); - } - - template - requires requires (Container& c) { - begin_container::call(c); - } - [[nodiscard]] static constexpr container_iterator_t - operator()(Container& c) - noexcept(noexcept(begin_container::call(c))) - { - static_assert(std::same_as::call(c)), container_iterator_t>); - return begin_container::call(c); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::begin_fn begin{}; - -} // cpos - -// Customization point -template -struct end_container; // not defined - -namespace detail { - -struct end_fn -{ - template - requires requires(Container& c) { - std::ranges::end(c); - } - [[nodiscard]] static constexpr container_sentinel_t - operator()(Container& c) - noexcept(noexcept(std::ranges::end(c))) - { - static_assert(std::same_as>); - return std::ranges::end(c); - } - - template - requires requires (Container& c) { - end_container::call(c); - } - [[nodiscard]] static constexpr container_sentinel_t - operator()(Container& c) - noexcept(noexcept(end_container::call(c))) - { - static_assert(std::same_as::call(c)), container_sentinel_t>); - return end_container::call(c); - } -}; - -} // detail - -inline namespace cpos { - -[[maybe_unused]] inline constexpr detail::end_fn end{}; - -} // cpos - -// ------------------------------------------------- - -// This is NOT a customization point. Don't specialize this. -template -struct is_container : std::false_type -{ - static_assert(!std::is_reference_v); - static_assert(!std::is_const_v); -}; - -template -struct is_container> - : is_container -{}; - -template - requires - std::default_initializable && - - requires(T& c) { - typename container_value::type; - traits::begin(c); - requires std::forward_iterator; - traits::end(c); - requires std::sentinel_for; - traits::is_empty(c); - traits::push_back(c, std::declval::type>()); - traits::append( - c, - std::declval(), - std::declval() - ); - traits::clear(c); - } -struct is_container : std::true_type -{}; - -// The attribute category type for `unused_container_type` is -// `container_attribute`, but it does not satisfy `is_container`. - -template -constexpr bool is_container_v = is_container::value; - -template -concept X4Container = is_container_v>; +// The attribute category of `unused_container_type` is `container_tag`, but it is not `X4Container` // ------------------------------------------------- diff --git a/modules/iris b/modules/iris index 6917f1f78..cb806724f 160000 --- a/modules/iris +++ b/modules/iris @@ -1 +1 @@ -Subproject commit 6917f1f78dff6ef5f85a12b533418be7b3297ffb +Subproject commit cb806724f2a2f5104cd5948958b642001b36eb25 diff --git a/test/x4/CMakeLists.txt b/test/x4/CMakeLists.txt index 31d8f5262..a7eb719e2 100644 --- a/test/x4/CMakeLists.txt +++ b/test/x4/CMakeLists.txt @@ -4,13 +4,19 @@ # Distributed under the Boost Software License, Version 1.0. # https://www.boost.org/LICENSE_1_0.txt +add_library(x4_test_pch OBJECT pch.cpp) +set_target_properties(x4_test_pch PROPERTIES CXX_EXTENSIONS OFF) +target_link_libraries(x4_test_pch PRIVATE Iris::X4 iris_cxx_test) +target_precompile_headers(x4_test_pch PUBLIC pch.hpp) + function(x4_define_test_headers test_name) iris_define_test_headers(x4_${test_name} PRIVATE FILE_SET HEADERS FILES ${ARGN}) endfunction() function(x4_define_test test_name) iris_define_test(x4_${test_name} ${ARGN}) - iris_define_test_headers(x4_${test_name} iris_x4_test.hpp) + iris_define_test_headers(x4_${test_name} iris_x4_test.hpp iris_x4_test_parse_debug.hpp) + target_precompile_headers(x4_${test_name}_test REUSE_FROM x4_test_pch) target_link_libraries(x4_${test_name}_test PRIVATE Iris::X4) set_target_properties(x4_${test_name}_test PROPERTIES FOLDER "test/x4") @@ -61,7 +67,6 @@ x4_define_tests( lexeme lit matches - move_to not_predicate no_case no_skip @@ -82,7 +87,6 @@ x4_define_tests( sequence skip smart_ptr - substitution symbols1 symbols2 symbols3 @@ -95,6 +99,7 @@ x4_define_tests( with with_local without + write_attribute x3_rule_problem ) diff --git a/test/x4/actions.cpp b/test/x4/actions.cpp index 96fb0fdc1..c1c3b25b5 100644 --- a/test/x4/actions.cpp +++ b/test/x4/actions.cpp @@ -9,15 +9,104 @@ #include "iris_x4_test.hpp" +#include +#include +#include #include +#include #include #include +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace visit_attr_test { + +struct NumberLit +{ + long long value = 0; + bool operator==(NumberLit const&) const = default; +}; + +struct StringLit +{ + std::string value; + bool operator==(StringLit const&) const = default; +}; + +struct Ident +{ + std::string name; + bool operator==(Ident const&) const = default; +}; + +using Literal = iris::rvariant; +using Primary = iris::rvariant; + +} // visit_attr_test + +IRIS_ALLOY_ADAPT_STRUCT(visit_attr_test::NumberLit, value); +IRIS_ALLOY_ADAPT_STRUCT(visit_attr_test::StringLit, value); +IRIS_ALLOY_ADAPT_STRUCT(visit_attr_test::Ident, name); + +namespace visit_attr_test { + +// Succeeds with an `int` ("7x", "0x"), or without an attribute ("5y") +constexpr auto five = (x4::int_ >> 'x') | (x4::lit('5') >> 'y'); + +// The example in README +constexpr auto timeout = ((x4::int_ >> "ms") | x4::lit("auto")).on_match([](auto&& ctx) { + x4::visit_attr( + ctx, + [](int& ms) { ms = std::min(ms, 1000); }, + [](x4::unused_type) {} // corresponds to the "auto" branch + ); +}); + +using Value = iris::rvariant; + +constexpr x4::rule value_rule = "value_rule"; +constexpr auto value_rule_def = value_rule = x4::int_ | +x4::standard::alpha; +IRIS_X4_DEFINE(value_rule) + +constexpr auto quoted = x4::lexeme['"' >> *~x4::standard::char_('"') >> '"']; +constexpr auto number_lit = x4::as(x4::long_long); +constexpr auto string_lit = x4::as(quoted); +constexpr auto ident = x4::as(x4::lexeme[x4::as(x4::standard::alpha >> *x4::standard::alnum)]); + +constexpr x4::rule literal_rule = "literal_rule"; +constexpr auto literal_rule_def = literal_rule = number_lit | string_lit; +IRIS_X4_DEFINE(literal_rule) + +constexpr auto literal_as = x4::as(number_lit | string_lit); + +// What an action saw: the `int` written, or none +using seen_ints = std::vector>; + +} // visit_attr_test + TEST_CASE("action") { using x4::int_; + using x4::standard::digit; + using x4::standard::space; IRIS_X4_ASSERT_CONSTEXPR_CTORS(x4::int_.on_match(std::true_type{})); @@ -35,7 +124,7 @@ TEST_CASE("action") next = x4::_attr(ctx); }; - REQUIRE(parse(input, x4::int_.on_match(fail) | x4::digit.on_match(setnext), x4::space).is_partial_match()); + REQUIRE(parse(input, int_.on_match(fail) | digit.on_match(setnext), space).is_partial_match()); CHECK(next == '1'); } @@ -50,3 +139,331 @@ TEST_CASE("action") CHECK(st.val == 42); } } + +TEST_CASE("visit_attr") +{ + using namespace visit_attr_test; + using x4::int_; + using x4::lit; + using x4::lexeme; + using iris::rvariant; + + seen_ints seen; + auto const observe = [&](auto&& ctx) { + STATIC_CHECK(x4::detail::is_action_attribute_view_v); + STATIC_CHECK(std::is_const_v>); + x4::visit_attr( + ctx, + [&](int& value) { seen.emplace_back(value); }, + [&](x4::unused_type) { seen.emplace_back(std::nullopt); } + ); + }; + auto const times_ten = [](auto&& ctx) { + x4::visit_attr(ctx, [](int& value) { value *= 10; }, [](x4::unused_type) {}); + }; + + // The action is called with the int written, including 0, or with no attribute + { + int number = 99; + REQUIRE(parse("7x", five.on_match(observe), number)); + CHECK(number == 7); + REQUIRE(parse("0x", five.on_match(observe), number)); + CHECK(number == 0); + number = 99; + REQUIRE(parse("5y", five.on_match(observe), number)); + CHECK(number == 0); + CHECK(seen == seen_ints{7, 0, std::nullopt}); + } + + // A changed value is written, a value changed to 0 as well + { + int ms = 0; + REQUIRE(parse("300ms", timeout, ms)); + CHECK(ms == 300); + REQUIRE(parse("5000ms", timeout, ms)); + CHECK(ms == 1000); + REQUIRE(parse("auto", timeout, ms)); + CHECK(ms == 0); + + auto const to_zero = [](auto&& ctx) { + x4::visit_attr(ctx, [](int& value) { value = 0; }, [](x4::unused_type) {}); + }; + std::optional optional_number; + REQUIRE(parse("7x", five.on_match(to_zero), optional_number)); + CHECK(optional_number == 0); + REQUIRE(parse("5y", five.on_match(to_zero), optional_number)); + CHECK(optional_number == std::nullopt); + } + + // The action sees the same in a sequence and a repetition + { + seen.clear(); + rvariant variant; + REQUIRE(parse("<7x>", '<' >> five.on_match(observe) >> '>', variant)); + CHECK(variant == rvariant{7}); + REQUIRE(parse("<5y>", '<' >> five.on_match(observe) >> '>', variant)); + CHECK(variant == rvariant{std::string{}}); + + std::vector ints; + REQUIRE(parse("7x5y0x", *five.on_match(observe), ints)); + CHECK(ints == std::vector{7, 0}); + CHECK(seen == seen_ints{7, std::nullopt, 7, std::nullopt, 0}); + } + + // No attribute: the whole destination is in its default state, and a container gets no element + { + int number = 99; + REQUIRE(parse("5y", five.on_match(observe), number)); + CHECK(number == 0); + std::optional optional_number = 99; + REQUIRE(parse("5y", five.on_match(observe), optional_number)); + CHECK(optional_number == std::nullopt); + rvariant variant = 99; + REQUIRE(parse("5y", five.on_match(observe), variant)); + CHECK(variant == rvariant{std::string{}}); + + std::vector ints; + REQUIRE(parse("5y5y", +five.on_match(observe), ints)); + CHECK(ints.empty()); + REQUIRE(parse("7x5y", +five.on_match(observe), ints)); + CHECK(ints == std::vector{7}); + REQUIRE(parse("1,5y,2", ((int_ >> 'x') | (lit('5') >> 'y') | int_).on_match(observe) % ',', ints)); + CHECK(ints == std::vector{1, 2}); + } + + // The same in a sequence of two attributes, whose other element is kept: the destination + // is not prepared here, as `parse` is called directly + { + constexpr std::string_view input = "7,5y"; + alloy::tuple numbers{1, 99}; + auto first = input.begin(); + REQUIRE((int_ >> ',' >> five.on_match(observe)).parse(first, input.end(), x4::unused, numbers)); + CHECK(first == input.end()); + CHECK(numbers == alloy::tuple{7, 0}); + } + + // Through `lexeme`, a sequence of one attribute and the left of a difference + { + seen.clear(); + int number = 0; + REQUIRE(parse("7x", lexeme[five].on_match(observe), number)); + CHECK(number == 7); + REQUIRE(parse("5y", lexeme[five.on_match(observe)], number)); + CHECK(number == 0); + REQUIRE(parse("<5y>", ('<' >> five >> '>').on_match(observe), number)); + CHECK(number == 0); + REQUIRE(parse("<7x>", ('<' >> five >> '>').on_match(times_ten), number)); + CHECK(number == 70); + REQUIRE(parse("7x", (five - lit("9")).on_match(observe), number)); + CHECK(number == 7); + CHECK(seen == seen_ints{7, std::nullopt, std::nullopt, 7}); + } + + // The inner action's change and whether it wrote reach the outer action + { + seen.clear(); + int number = 0; + REQUIRE(parse("7x", five.on_match(times_ten).on_match(observe), number)); + CHECK(number == 70); + REQUIRE(parse("5y", five.on_match(times_ten).on_match(observe), number)); + CHECK(number == 0); + REQUIRE(parse("7x", (five.on_match(times_ten) | lit('z')).on_match(observe), number)); + CHECK(number == 70); + REQUIRE(parse("z", (five.on_match(times_ten) | lit('z')).on_match(observe), number)); + CHECK(number == 0); + CHECK(seen == seen_ints{70, std::nullopt, 70, std::nullopt}); + } + + // An action which rejects the match after changing the value: the change is not written + { + auto const reject_zero = [](auto&& ctx) { + bool accepted = true; + x4::visit_attr( + ctx, + [&](int& value) { + accepted = value != 0; + value = 99; + }, + [](x4::unused_type) {} + ); + return accepted; + }; + + std::vector ints; + auto const res = x4::parse(std::string_view("7x0x"), *five.on_match(reject_zero), ints); + REQUIRE(res.ok); + CHECK(res.remainder_str() == "0x"); + CHECK(ints == std::vector{99}); + + int number = -1; + REQUIRE(parse("0x", five.on_match(reject_zero) | (int_ >> 'x'), number)); + CHECK(number == 0); + + ints.clear(); + REQUIRE(parse("0x7x", *(five.on_match(reject_zero) | (int_ >> 'x')), ints)); + CHECK(ints == std::vector{0, 99}); + } + + // An action on a subject which always writes keeps `int&`; with no destination, the action + // still sees the attribute written + { + int number = 0; + REQUIRE(parse("7", int_.on_match([](auto&& ctx) { + STATIC_CHECK(std::is_same_v); + x4::_attr(ctx) += 1; + }), number)); + CHECK(number == 8); + + seen.clear(); + REQUIRE(parse("7x5y", x4::omit[*five.on_match(observe)], x4::unused)); + REQUIRE(parse("0x", five.on_match(observe), x4::unused)); + CHECK(seen == seen_ints{7, std::nullopt, 0}); + } +} + +TEST_CASE("visit_attr candidates") +{ + using namespace visit_attr_test; + using x4::int_; + using x4::double_; + using x4::lit; + using x4::standard::alpha; + using iris::rvariant; + + // The candidates of an alternative are passed one by one; a declared variant is one candidate + { + std::vector seen; + auto const change_declared = [&](auto&& ctx) { + x4::visit_attr( + ctx, + [&](Value& value) { + seen.emplace_back(value.index() == 0 ? "Value int" : "Value string"); + value = 42; + }, + [&](x4::unused_type) { seen.emplace_back("none"); } + ); + }; + auto const observe_declared_or_double = [&](auto&& ctx) { + x4::visit_attr( + ctx, + [&](Value&) { seen.emplace_back("Value"); }, + [&](double& value) { seen.emplace_back("double " + std::to_string(static_cast(value))); }, + [&](x4::unused_type) { seen.emplace_back("none"); } + ); + }; + + Value value; + auto const flat = (lit("null") | int_ | +alpha).on_match([&](auto&& ctx) { + x4::visit_attr( + ctx, + [&](int& number) { seen.emplace_back("int " + std::to_string(number)); }, + [&](std::string& string) { seen.emplace_back("string " + string); }, + [&](x4::unused_type) { seen.emplace_back("none"); } + ); + }); + REQUIRE(parse("1", flat, value)); + REQUIRE(parse("ab", flat, value)); + REQUIRE(parse("null", flat, value)); + + auto const declared_as = (lit("null") | x4::as(int_ | +alpha)).on_match(change_declared); + auto const declared_rule = (lit("null") | value_rule).on_match(change_declared); + REQUIRE(parse("ab", declared_as, value)); + CHECK(value == Value{42}); + REQUIRE(parse("ab", declared_rule, value)); + CHECK(value == Value{42}); + REQUIRE(parse("null", declared_rule, value)); + CHECK(value == Value{}); + + using value_or_double = rvariant; + value_or_double destination; + auto const declared_as_or_double = (lit("null") | x4::as(int_ | +alpha) | ('#' >> double_)).on_match(observe_declared_or_double); + auto const declared_rule_or_double = (lit("null") | value_rule | ('#' >> double_)).on_match(observe_declared_or_double); + REQUIRE(parse("#2", declared_as_or_double, destination)); + CHECK(destination == value_or_double{2.0}); + REQUIRE(parse("ab", declared_as_or_double, destination)); + CHECK(destination == value_or_double{Value{std::string("ab")}}); + REQUIRE(parse("7", declared_rule_or_double, destination)); + CHECK(destination == value_or_double{Value{7}}); + + CHECK(seen == std::vector{ + "int 1", "string ab", "none", + "Value string", "Value string", "none", + "double 2", "Value", "Value", + }); + } + + { + auto const check_number = [](auto const& number) { + long long value = -1; + REQUIRE(parse("42", number, value)); + CHECK(value == 42); + REQUIRE(parse("12345678901", number, value)); // `long_long`, as `int_` overflows + CHECK(value == 12345678901); + value = -1; + REQUIRE(parse("null", number, value)); + CHECK(value == 0); + + std::vector values; + REQUIRE(parse("42,12345678901,null", number % ',', values)); + CHECK(values == std::vector{42, 12345678901}); + }; + auto const number = int_ | x4::long_long | lit("null"); + check_number(number); + check_number(number.on_match([] {})); + } + + // `Literal&`, `Ident&` or none, written into `Primary` and `std::vector` + { + std::vector seen; + auto const observe = [&](auto&& ctx) { + x4::visit_attr( + ctx, + [&](Literal& literal) { seen.emplace_back(literal.index() == 0 ? "NumberLit" : "StringLit"); }, + [&](Ident&) { seen.emplace_back("Ident"); }, + [&](x4::unused_type) { seen.emplace_back("none"); } + ); + }; + Primary const number_literal{Literal{NumberLit{1}}}; + Primary const string_literal{Literal{StringLit{"s"}}}; + Primary const identifier{Ident{"y"}}; + + auto const check_primary = [&](auto const& literal) { + auto const primary = (lit("null") | literal | ident).on_match(observe); + Primary attr; + REQUIRE(parse("\"s\"", primary, attr)); + CHECK(attr == string_literal); + REQUIRE(parse("y", primary, attr)); + CHECK(attr == identifier); + REQUIRE(parse("null", primary, attr)); + CHECK(attr == Primary{}); + + std::vector primaries; + REQUIRE(parse("1,null,\"s\",y", primary % ',', primaries)); + CHECK(primaries == std::vector{number_literal, string_literal, identifier}); + }; + check_primary(literal_rule); + check_primary(literal_as); + + std::vector const once{"StringLit", "Ident", "none", "NumberLit", "none", "StringLit", "Ident"}; + std::vector twice = once; + twice.insert(twice.end(), once.begin(), once.end()); + CHECK(seen == twice); + } + + // An explicit empty array is one element; no attribute adds none + { + using Array = std::vector; + std::vector seen; + auto const array = ((lit('[') >> (int_ % ',') >> ']') | (lit("[]") >> x4::default_value) | lit("null")).on_match([&](auto&& ctx) { + x4::visit_attr( + ctx, + [&](Array& elements) { seen.emplace_back("Array " + std::to_string(elements.size())); }, + [&](x4::unused_type) { seen.emplace_back("none"); } + ); + }); + std::vector arrays; + REQUIRE(parse("[];[1,2];null;[]", array % ';', arrays)); + CHECK(arrays == std::vector{{}, {1, 2}, {}}); + CHECK(seen == std::vector{"Array 0", "Array 2", "none", "Array 0"}); + } +} diff --git a/test/x4/alternative.cpp b/test/x4/alternative.cpp index 600373ebe..b937857c7 100644 --- a/test/x4/alternative.cpp +++ b/test/x4/alternative.cpp @@ -12,30 +12,37 @@ #include +#include #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include +#include #include #include #include @@ -62,7 +69,83 @@ struct alloy::adaptor using getters_list = iris::constant_list<&di_include::FileName>; }; -struct undefined {}; +struct undefined +{ + bool operator==(undefined const&) const = default; +}; + +struct Object +{ + [[maybe_unused]] std::string name; + bool operator==(Object const&) const = default; +}; + +namespace declared_variant { + +struct NumberLit +{ + long long value = 0; + bool operator==(NumberLit const&) const = default; +}; + +struct StringLit +{ + std::string value; + bool operator==(StringLit const&) const = default; +}; + +struct Ident +{ + std::string name; + bool operator==(Ident const&) const = default; +}; + +using Literal = iris::rvariant; +using Primary = iris::rvariant; + +struct Call +{ + Ident callee; + std::vector args; +}; + +using Scalar = iris::rvariant; + +} // declared_variant + +IRIS_ALLOY_ADAPT_STRUCT(declared_variant::NumberLit, value); +IRIS_ALLOY_ADAPT_STRUCT(declared_variant::StringLit, value); +IRIS_ALLOY_ADAPT_STRUCT(declared_variant::Ident, name); +IRIS_ALLOY_ADAPT_STRUCT(declared_variant::Call, callee, args); + +namespace declared_variant { + +template +using attribute_t = x4::parser_traits>::attribute_type; + +template +using candidates_t = x4::detail::attribute_candidates_t>; + +constexpr auto quoted = x4::lexeme['"' >> *~x4::standard::char_('"') >> '"']; +constexpr auto number_lit = x4::as(x4::long_long); +constexpr auto string_lit = x4::as(quoted); +constexpr auto ident = x4::as(x4::lexeme[x4::as(x4::standard::alpha >> *x4::standard::alnum)]); + +constexpr x4::rule literal_rule = "literal_rule"; +constexpr auto literal_rule_def = literal_rule = number_lit | string_lit; +IRIS_X4_DEFINE(literal_rule) + +constexpr auto literal_as = x4::as(number_lit | string_lit); + +constexpr auto integer = x4::lexeme[x4::long_long >> !x4::lit('.')]; + +constexpr x4::rule scalar_rule = "scalar_rule"; +constexpr auto scalar_rule_def = scalar_rule = integer | x4::double_; +IRIS_X4_DEFINE(scalar_rule) + +constexpr auto scalar_as = x4::as(integer | x4::double_); + +} // declared_variant TEST_CASE("alternative") { @@ -91,6 +174,34 @@ TEST_CASE("alternative") STATIC_CHECK(std::same_as::attribute_type, rvariant>); STATIC_CHECK(std::same_as::attribute_type, rvariant>); + { + // `T` and `recursive_wrapper` are one alternative; the wrapped form is kept, at the first position + using iris::recursive_wrapper; + using x4::as; + using x4::standard::alpha; + constexpr auto plain = as(+alpha); + constexpr auto wrapped = as>(lit('#') >> +alpha); + + STATIC_CHECK(std::same_as::attribute_type, recursive_wrapper>); + STATIC_CHECK(std::same_as::attribute_type, recursive_wrapper>); + STATIC_CHECK(std::same_as::attribute_type, rvariant, int>>); + STATIC_CHECK(std::same_as::attribute_type, rvariant>>); + + recursive_wrapper w; + REQUIRE(parse("abc", plain | wrapped, w)); + CHECK(w->FileName == "abc"); + REQUIRE(parse("#xyz", plain | wrapped, w)); + CHECK(w->FileName == "xyz"); + + rvariant, int> v; + REQUIRE(parse("#xyz", plain | int_ | wrapped, v)); + CHECK(iris::get(v).FileName == "xyz"); + REQUIRE(parse("42", plain | int_ | wrapped, v)); + CHECK(iris::get(v) == 42); + REQUIRE(parse("abc", plain | int_ | wrapped, v)); + CHECK(iris::get(v).FileName == "abc"); + } + IRIS_X4_ASSERT_CONSTEXPR_CTORS(char_ | char_); { @@ -116,11 +227,6 @@ TEST_CASE("alternative") REQUIRE(parse("12345", lit("rock") | int_ | char_, v)); CHECK(iris::get(v) == 12345); } - { - attr_type v; - REQUIRE(parse("rock", lit("rock") | int_ | char_, v)); - CHECK(v.index() == 0); - } { attr_type v; REQUIRE(parse("x", lit("rock") | int_ | char_, v)); @@ -256,8 +362,7 @@ TEST_CASE("alternative") using attribute_type = x4::parser_traits::attribute_type; STATIC_CHECK(std::same_as>); - using substitute_type = x4::variant_find_holdable_type::type; - STATIC_CHECK(std::same_as>); + STATIC_CHECK(x4::detail::variant_alternative_for_v == 1); Attr var; REQUIRE(parse("truetrue", parser, var)); @@ -270,8 +375,7 @@ TEST_CASE("alternative") using attribute_type = x4::parser_traits::attribute_type; STATIC_CHECK(std::same_as); - using substitute_type = x4::variant_find_holdable_type::type; - STATIC_CHECK(std::same_as); + STATIC_CHECK(x4::detail::variant_alternative_for_v == 1); Attr var; REQUIRE(parse("123", parser, var)); @@ -312,18 +416,6 @@ TEST_CASE("alternative") CHECK(st.val == 42); } - { - // attributeless parsers must not insert values - std::vector v; - REQUIRE(parse("1 2 3 - 5 - - 7 -", (int_ | '-') % ' ', v)); - REQUIRE(v.size() == 5); - CHECK(v[0] == 1); - CHECK(v[1] == 2); - CHECK(v[2] == 3); - CHECK(v[3] == 5); - CHECK(v[4] == 7); - } - { // regressing test for #603 struct X {}; @@ -362,6 +454,29 @@ TEST_CASE("alternative") Bar x; CHECK(parse("abaabb", +('a' >> fixed_value(Foo{}) | 'b' >> fixed_value(int{})), x)); } + + { + constexpr auto op = x4::as(lit("==") | lit('<')); + std::string s; + REQUIRE(parse("==", op, s)); + CHECK(s.empty()); + REQUIRE(parse("<", op, s)); + CHECK(s.empty()); + } + { + constexpr auto op = x4::as(x4::string("==") | x4::string("<")); + std::string s; + REQUIRE(parse("==", op, s)); + CHECK(s == "=="); + REQUIRE(parse("<", op, s)); + CHECK(s == "<"); + } + { + // a failed branch is undone + std::string s; + REQUIRE(parse("ay", (char_ >> 'x') | (char_ >> 'y'), s)); + CHECK(s == "a"); + } } TEST_CASE("alternative of same attributes (a | a)") @@ -435,3 +550,380 @@ TEST_CASE("alternative of same attributes (a | a)") CHECK(var == decltype(var){"foo", {true, false}}); } } + +namespace { + +// The reference for `x4::alternative` without the attribute shared between the branches, for the +// branches with an attribute: +// each branch parses into its own attribute, made fresh by `x4::parse`, and the first match +// is the result. On failure, the result is the default state, as `x4::parse` leaves it. +template +struct separate_branches_result +{ + bool ok = false; + std::size_t rest = 0; + Attr attr{}; +}; + +template +separate_branches_result parse_separate_branches(std::string_view input, Branches const&... branches) +{ + // A branch without an attribute leaves the default of the attribute of the alternative, not + // of its own; see "attributeless branch leaves the default" + static_assert((x4::has_attribute_v && ...)); + + separate_branches_result result; + auto parse_branch = [&](auto const& branch) { + Attr attr{}; + auto const res = x4::parse(input, branch, attr); + if (!res.ok) return false; + result = {.ok = true, .rest = res.remainder.size(), .attr = std::move(attr)}; + return true; + }; + (void)(parse_branch(branches) || ...); + return result; +} + +template +void check_separate_branches(std::string_view input, Parser const& parser, Branches const&... branches) +{ + CAPTURE(input); + auto const expected = parse_separate_branches(input, branches...); + + Attr attr{}; + auto const res = x4::parse(input, parser, attr); + REQUIRE(res.ok == expected.ok); + if (res.ok) { + CHECK(res.remainder.size() == expected.rest); + } + CHECK(attr == expected.attr); +} + +} // anonymous + +TEST_CASE("alternative attribute reuse") +{ + using x4::standard::alpha; + using x4::standard::digit; + using x4::lit; + using x4::int_; + using x4::eps; + using iris::rvariant; + + // The later branch writes into what the failed branch wrote + { + constexpr auto excl = +alpha >> '!'; + constexpr auto quest = +alpha >> '?'; + for (std::string_view input : {"ab?", "ab!", "ab."}) { + check_separate_branches>(input, excl | quest | int_, excl, quest, int_); + check_separate_branches(input, excl | quest, excl, quest); + check_separate_branches>>( + std::string{"1,"} + std::string{input}, + int_ >> ',' >> (excl | quest), + int_ >> ',' >> excl, int_ >> ',' >> quest + ); + } + } + { + constexpr auto excl = int_ >> ',' >> int_ >> '!'; + constexpr auto quest = int_ >> ',' >> int_ >> '?'; + check_separate_branches>("1,2?", excl | quest, excl, quest); + check_separate_branches>("1,2.", excl | quest, excl, quest); + } + { + constexpr auto excl = (int_ % ',') >> '!'; + constexpr auto quest = (int_ % ',') >> '?'; + check_separate_branches>("1,2?", excl | quest, excl, quest); + // into the held alternative, converted from another type + check_separate_branches, std::string>>("1,2?", excl | -quest, excl, -quest); + } + { + // a disengaged optional, converted into the attribute + constexpr auto int_x = int_ >> 'x'; + check_separate_branches("5y", int_x | -(int_ >> 'z'), int_x, -(int_ >> 'z')); + } + { + // into a container which holds the preceding elements + constexpr auto excl = +digit >> '!'; + constexpr auto quest = +digit >> '?'; + check_separate_branches("ab12?", +alpha >> (excl | quest), +alpha >> excl, +alpha >> quest); + } + + // The later branch writes another alternative + { + constexpr auto int_x = int_ >> 'x'; + constexpr auto one_alpha = lit('1') >> +alpha; + check_separate_branches>("1ab", int_x | one_alpha, int_x, one_alpha); + + constexpr auto alpha_excl = +alpha >> '!'; + constexpr auto a_int = lit('a') >> int_; + check_separate_branches>("a1", alpha_excl | a_int, alpha_excl, a_int); + } + +} + +template +void check_root_and_slot(std::string_view input, Parser const& parser, Attr const& expected) +{ + CAPTURE(input); + { + Attr attr{}; + REQUIRE(x4::parse(input, parser, attr).ok); + CHECK(attr == expected); + } + { + alloy::tuple attr{}; + std::string const slot_input = "1," + std::string{input}; + REQUIRE(x4::parse(slot_input, x4::int_ >> ',' >> parser, attr).ok); + CHECK(alloy::get<1>(attr) == expected); + } +} + +TEST_CASE("attributeless branch leaves the default") +{ + using x4::standard::alpha; + using x4::standard::digit; + using x4::standard::char_; + using x4::lit; + using x4::int_; + using x4::eps; + using x4::lexeme; + using x4::fixed_value; + using x4::default_value; + using iris::rvariant; + + // The attribute is left in its default state, not in the part written by the other branches + // (nor by the failed branch) + constexpr auto five = (int_ >> 'x') | (lit('5') >> 'y'); + check_root_and_slot("5y", five, 0); + check_root_and_slot("5y", five, std::optional{}); + check_root_and_slot("5y", five, rvariant{}); + check_root_and_slot("5y", lexeme[five], rvariant{}); + check_root_and_slot("<5y>", lit('<') >> five >> '>', rvariant{}); + check_root_and_slot("5y", lexeme[five] | +alpha, rvariant{}); + check_root_and_slot("5y", five - lit('q'), std::optional{}); + check_root_and_slot("5y", five - lit('q'), rvariant{}); + + // Nor in the parts or the elements written by the failed branch + check_root_and_slot("1,2?", (int_ >> ',' >> int_ >> '!') | lit("1,2?"), alloy::tuple{}); + check_root_and_slot("ab?", (+alpha >> '!') | eps, std::string{}); + + using undefined_int_char = rvariant; + check_root_and_slot("rock", lit("rock") | int_ | char_, undefined_int_char{}); + check_root_and_slot("rock", lit("rock") | int_, undefined_int_char{}); + check_root_and_slot("12", lit("rock") | int_, undefined_int_char{12}); + + constexpr auto timeout = (int_ >> lit("ms")) | lit("auto"); + constexpr auto timeout_fixed = (int_ >> lit("ms")) | (lit("auto") >> fixed_value(std::optional{})); + constexpr auto timeout_default = (int_ >> lit("ms")) | (lit("auto") >> default_value>); + check_root_and_slot("auto", timeout, std::optional{}); + check_root_and_slot("auto", timeout_fixed, std::optional{}); + check_root_and_slot("auto", timeout_default, std::optional{}); + check_root_and_slot("10ms", timeout, std::optional{10}); + check_root_and_slot("10ms", timeout_fixed, std::optional{10}); + check_root_and_slot("10ms", timeout_default, std::optional{10}); + { + std::optional attr; + CHECK_FALSE(parse("", timeout, attr)); + } + + // `-p` keeps the attribute of `p` when it succeeds + check_root_and_slot("250ms", -timeout, std::optional{250}); + check_root_and_slot("0ms", -timeout, std::optional{0}); + check_root_and_slot("auto", -timeout, std::optional{}); + check_root_and_slot("", -timeout, std::optional{}); + check_root_and_slot("7", -int_, std::optional{7}); + check_root_and_slot("x", -int_, std::optional{}); + + // An empty array is written only when the grammar says so + using Array = std::vector; + constexpr auto array_parser = (lit('[') >> (int_ % ',') >> ']') | lit("[]"); + constexpr auto explicit_array_parser = (lit('[') >> (int_ % ',') >> ']') | (lit("[]") >> default_value); + check_root_and_slot("[]", array_parser, Array{}); + check_root_and_slot("[]", array_parser, std::optional{}); + check_root_and_slot("[]", array_parser, rvariant{}); + check_root_and_slot("[]", explicit_array_parser, Array{}); + check_root_and_slot("[]", explicit_array_parser, std::optional{Array{}}); + check_root_and_slot("[]", explicit_array_parser, rvariant{Array{}}); + check_root_and_slot("[1,2]", array_parser, Array{1, 2}); + check_root_and_slot("[1,2]", array_parser, std::optional{Array{1, 2}}); + check_root_and_slot("[1,2]", array_parser, rvariant{Array{1, 2}}); + check_root_and_slot("[1,2]", explicit_array_parser, Array{1, 2}); + check_root_and_slot("[1,2]", explicit_array_parser, std::optional{Array{1, 2}}); + check_root_and_slot("[1,2]", explicit_array_parser, rvariant{Array{1, 2}}); + + // Into a container, a branch without an attribute appends nothing + { + std::vector arrays; + REQUIRE(parse("[];[1,2];[]", explicit_array_parser % ';', arrays)); + CHECK(arrays == std::vector{{}, {1, 2}, {}}); + } + { + std::vector arrays; + REQUIRE(parse("[];[1,2];[]", lexeme[explicit_array_parser] % ';', arrays)); + CHECK(arrays == std::vector{{}, {1, 2}, {}}); + } + { + std::vector arrays; + REQUIRE(parse("<[]>;<[1,2]>;<[]>", (lit('<') >> explicit_array_parser >> '>') % ';', arrays)); + CHECK(arrays == std::vector{{}, {1, 2}, {}}); + } + { + // wrapped or not + std::vector ints; + REQUIRE(parse("5y", *five, ints)); + CHECK(ints.empty()); + REQUIRE(parse("5y", *lexeme[five], ints)); + CHECK(ints.empty()); + REQUIRE(parse("<5y>", *(lit('<') >> five >> '>'), ints)); + CHECK(ints.empty()); + REQUIRE(parse("7x5y8x", *lexeme[five], ints)); + CHECK(ints == std::vector{7, 8}); + } + { + // the elements of the failed repetition are not kept, and the preceding ones are + std::vector ints; + auto const res = parse("<7x><5y><8x", *(lit('<') >> five >> '>'), ints); + REQUIRE(res.ok); + CHECK(res.remainder.size() == 3); + CHECK(ints == std::vector{7}); + } + { + std::vector ints; + REQUIRE(parse("1 2 3 - 5 - - 7 -", (int_ | '-') % ' ', ints)); + CHECK(ints == std::vector{1, 2, 3, 5, 7}); + ints.clear(); + REQUIRE(parse("1 2 3 - 5 - - 7 -", lexeme[int_ | '-'] % ' ', ints)); // wrapped + CHECK(ints == std::vector{1, 2, 3, 5, 7}); + } + { + std::string str; + REQUIRE(parse("xab", +(lit('x') | char_), str)); + CHECK(str == "ab"); + } + { + // the preceding elements are kept, and those of the failed branch are not + std::string str; + auto const res = parse("ab12?", +alpha >> ((+digit >> '!') | eps), str); + REQUIRE(res.ok); + CHECK(res.remainder.size() == 3); + CHECK(str == "ab"); + } +} + +TEST_CASE("declared variant") +{ + using namespace declared_variant; + using x4::int_; + using x4::double_; + using x4::lit; + using x4::lexeme; + using x4::standard::alpha; + using iris::rvariant; + using iris::type_list; + + using Value = rvariant; + constexpr auto int_or_alpha = int_ | +alpha; + constexpr auto hashed_double = '#' >> double_; + constexpr x4::rule value_rule = "value_rule"; + + STATIC_CHECK(std::same_as, Value>); + STATIC_CHECK(std::same_as, type_list>); + STATIC_CHECK(std::same_as(int_or_alpha) | lit("null"))>, Value>); + STATIC_CHECK(std::same_as(int_or_alpha) | lit("null"))>, type_list>); + STATIC_CHECK(std::same_as(int_or_alpha) | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as(int_or_alpha) | lit("null")) | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as, Value>); + STATIC_CHECK(std::same_as, rvariant>); + STATIC_CHECK(std::same_as, type_list>); + STATIC_CHECK(std::same_as{1}) | +alpha)>, rvariant, std::string>>); + + STATIC_CHECK(std::same_as(int_or_alpha) | lit("null")])>, type_list>); + STATIC_CHECK(std::same_as(int_or_alpha) | lit("null")] | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as, rvariant>); + STATIC_CHECK(std::same_as, rvariant>); + STATIC_CHECK(std::same_as> (x4::as(int_or_alpha) | lit("null")) >> '>') | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as> int_or_alpha >> '>') | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as(int_or_alpha) - lit("x")) | hashed_double)>, rvariant>); + STATIC_CHECK(std::same_as, rvariant>); + + STATIC_CHECK(std::same_as> +alpha)>, type_list>>); + + Primary const number_literal{Literal{NumberLit{1}}}; + Primary const string_literal{Literal{StringLit{"s"}}}; + Primary const identifier{Ident{"y"}}; + + auto const check_primary = [&](auto const& literal) { + auto const primary = literal | ident; + STATIC_CHECK(std::same_as, Primary>); + + { + Primary attr; + REQUIRE(parse("1", primary, attr)); + CHECK(attr == number_literal); + REQUIRE(parse("\"s\"", primary, attr)); + CHECK(attr == string_literal); + REQUIRE(parse("y", primary, attr)); + CHECK(attr == identifier); + } + { + std::vector attr; + REQUIRE(parse("1,\"s\",y", primary % ',', attr)); + CHECK(attr == std::vector{number_literal, string_literal, identifier}); + } + { + std::vector attr; + REQUIRE(parse("1,\"s\",y", -(primary % ','), attr)); + CHECK(attr == std::vector{number_literal, string_literal, identifier}); + } + { + std::vector attr; + REQUIRE(parse("(1,\"s\",y)", '(' >> (primary % ',') >> ')', attr)); + CHECK(attr == std::vector{number_literal, string_literal, identifier}); + } + { + Call attr; + REQUIRE(parse("f(1,\"s\",y)", ident >> '(' >> -(primary % ',') >> ')', attr)); + CHECK(attr.callee == Ident{"f"}); + CHECK(attr.args == std::vector{number_literal, string_literal, identifier}); + } + { + auto const parenthesized = primary | ('(' >> primary >> ')'); + Primary attr; + REQUIRE(parse("(\"s\")", parenthesized, attr)); + CHECK(attr == string_literal); + REQUIRE(parse("(y)", parenthesized, attr)); + CHECK(attr == identifier); + REQUIRE(parse("\"s\"", parenthesized, attr)); + CHECK(attr == string_literal); + } + }; + check_primary(literal_rule); + check_primary(literal_as); + + using Nested = rvariant; + using Flat = rvariant; + + auto const check_setting = [](auto const& number) { + auto const setting = number | quoted; + STATIC_CHECK(std::same_as, Nested>); + + Nested nested; + REQUIRE(parse("42", setting, nested)); + CHECK(nested == Nested{Scalar{42LL}}); + REQUIRE(parse("4.5", setting, nested)); + CHECK(nested == Nested{Scalar{4.5}}); + REQUIRE(parse("\"on\"", setting, nested)); + CHECK(nested == Nested{std::string("on")}); + + Flat flat; + REQUIRE(parse("42", setting, flat)); + CHECK(flat == Flat{42LL}); + REQUIRE(parse("4.5", setting, flat)); + CHECK(flat == Flat{4.5}); + REQUIRE(parse("\"on\"", setting, flat)); + CHECK(flat == Flat{std::string("on")}); + }; + check_setting(scalar_rule); + check_setting(scalar_as); +} diff --git a/test/x4/as.cpp b/test/x4/as.cpp index 5d066c335..3a45b853a 100644 --- a/test/x4/as.cpp +++ b/test/x4/as.cpp @@ -23,6 +23,9 @@ #include #include #include +#include + +#include #include @@ -41,7 +44,6 @@ using x4::string; using It = std::string_view::const_iterator; using Se = It; -using Context = unused_type; constexpr auto do_nothing = [](auto&&) {}; constexpr auto disable_attr = eps.on_match([](auto&&) {}); @@ -147,6 +149,21 @@ TEST_CASE("as(p)") REQUIRE(p.parse(empty_input_first, empty_input_last, unused, result)); CHECK(result == 42); } + + { + constexpr auto p = x4::as(x4::int_); + constexpr std::string_view input = "7"; + + long long converted = 0; + auto first = input.begin(); + REQUIRE(p.parse(first, input.end(), unused, converted)); + CHECK(converted == 7); + + alloy::tuple single{0}; + first = input.begin(); + REQUIRE(p.parse(first, input.end(), unused, single)); + CHECK(alloy::get<0>(single) == 7); + } } TEST_CASE("as(as(p))") diff --git a/test/x4/attribute_contract.cpp b/test/x4/attribute_contract.cpp index d92175f99..5fa4c2c2a 100644 --- a/test/x4/attribute_contract.cpp +++ b/test/x4/attribute_contract.cpp @@ -27,6 +27,7 @@ #include "iris_x4_test.hpp" +#include #include #include #include @@ -54,6 +55,8 @@ #include #include +#include + namespace { struct Pair @@ -64,11 +67,11 @@ struct Pair bool operator==(Pair const&) const = default; }; -struct Single +struct SingleElement { int n; - bool operator==(Single const&) const = default; + bool operator==(SingleElement const&) const = default; }; // A plain type whose reset can throw: assignment is not `noexcept`. @@ -106,10 +109,47 @@ struct throwing_parser : x4::parser> template inline constexpr throwing_parser always_throw{}; +// A container of letters, passed to `std::string` by its conversion +struct letters : std::vector +{ + operator std::string() const { return {begin(), end()}; } +}; + +// A container of letters whose conversion to `std::string` turns them into upper case +struct shouted_letters : std::vector +{ + operator std::string() const + { + std::string shouted(begin(), end()); + for (char& c : shouted) { + c = static_cast(std::toupper(static_cast(c))); + } + return shouted; + } +}; + } // anonymous IRIS_ALLOY_ADAPT_STRUCT(Pair, a, b); -IRIS_ALLOY_ADAPT_STRUCT(Single, n); +IRIS_ALLOY_ADAPT_STRUCT(SingleElement, n); + +// A rule whose value is assembled by an action, and a rule which fails after writing a part of its value +constexpr iris::x4::rule assembled_word = "assembled_word"; +constexpr iris::x4::rule banged_word = "banged_word"; +constexpr iris::x4::rule letters_word = "letters_word"; +constexpr iris::x4::rule shouted_word = "shouted_word"; + +constexpr auto assembled_word_def = assembled_word = (+iris::x4::standard::alpha).on_match([](auto&& ctx) { + iris::x4::_rule_var(ctx) = iris::x4::_attr(ctx); +}); +constexpr auto banged_word_def = banged_word = +iris::x4::standard::alpha >> '!'; +constexpr auto letters_word_def = letters_word = +iris::x4::standard::alpha; +constexpr auto shouted_word_def = shouted_word = +iris::x4::standard::alpha; + +IRIS_X4_DEFINE(assembled_word) +IRIS_X4_DEFINE(banged_word) +IRIS_X4_DEFINE(letters_word) +IRIS_X4_DEFINE(shouted_word) using namespace std::string_literals; using namespace std::string_view_literals; @@ -167,7 +207,7 @@ TEST_CASE("attribute contract: prior content does not influence the result") X4_TEST_SUCCESS(std::optional{999}, "42", -int_, std::optional{42}); X4_TEST_SUCCESS(Pair(999, "poison"s), "42abc", int_ >> +alpha, Pair(42, "abc"s)); - X4_TEST_SUCCESS(Single{999}, "42", int_, Single{42}); + X4_TEST_SUCCESS(SingleElement{999}, "42", int_, SingleElement{42}); { std::unique_ptr attr = std::make_unique(999); @@ -186,6 +226,24 @@ TEST_CASE("attribute contract: prior content does not influence the result") REQUIRE(parse("", fixed_value(iris::rvariant{42}), attr)); CHECK(attr == wide_t{42}); } + { + // prepared to the first alternative, not the alternative held before + var_t attr{"poison"s}; + REQUIRE(parse("", x4::rule{} = eps, attr)); + CHECK(attr == var_t{}); + } + { + // a container is emptied by `clear()`, which keeps the allocated capacity (also on a failed parse) + std::vector attr({7, 8, 9}); + attr.reserve(100); + auto const capacity = attr.capacity(); + REQUIRE(parse("1,2", int_ % lit(','), attr)); + CHECK(attr == std::vector({1, 2})); + CHECK(attr.capacity() == capacity); + REQUIRE(!parse("1,2", (int_ % lit(',')) >> lit('!'), attr)); + CHECK(attr.empty()); + CHECK(attr.capacity() == capacity); + } } TEST_CASE("attribute contract: a failed parse resets the attribute") @@ -200,7 +258,7 @@ TEST_CASE("attribute contract: a failed parse resets the attribute") X4_TEST_FAILURE(std::optional{999}, "x", int_); X4_TEST_FAILURE(Pair(999, "poison"s), "42123", int_ >> +alpha); - X4_TEST_FAILURE(Single{999}, "x", int_); + X4_TEST_FAILURE(SingleElement{999}, "x", int_); { std::unique_ptr attr = std::make_unique(999); @@ -254,6 +312,9 @@ TEST_CASE("attribute contract: parser depending on the previous result of the su X4_TEST_SUCCESS("poison"s, "ab", +(alpha >> digit | alpha), "ab"s); X4_TEST_SUCCESS("poison"s, "a,b", (alpha >> digit | alpha) % lit(','), "ab"s); X4_TEST_SUCCESS("poison"s, "xab", x4::as(alpha >> (alpha >> digit | alpha)) >> lit('b'), "xa"s); + X4_TEST_SUCCESS("poison"s, "ab?", banged_word | (+alpha >> lit('?')), "ab"s); + X4_TEST_SUCCESS("poison"s, "ab?", x4::as(+alpha >> lit('!')) | (+alpha >> lit('?')), "ab"s); + X4_TEST_SUCCESS("poison"s, "ab?", -banged_word >> lit("ab?"), ""s); // Same as above, where the branch attribute is a variant and the element type is a wider variant { @@ -266,6 +327,43 @@ TEST_CASE("attribute contract: parser depending on the previous result of the su X4_TEST_SUCCESS(std::vector{}, "12ab,1!", (expr >> lit('!') | +alnum) % lit(','), std::vector({stmt_t{"12ab"s}, stmt_t{expr_t{1}}})); } + // A variant selects the same type over a single-element tuple-like of it, whatever the order + { + using single_element_or_plain = iris::rvariant; + using plain_or_single_element = iris::rvariant; + X4_TEST_SUCCESS(single_element_or_plain{}, "12", int_, single_element_or_plain{12}); + X4_TEST_SUCCESS(plain_or_single_element{}, "12", int_, plain_or_single_element{12}); + X4_TEST_SUCCESS(std::vector{}, "1,2", int_ % lit(','), std::vector({single_element_or_plain{1}, single_element_or_plain{2}})); + } + + // A rule or `as` assembles its own value, which is appended to the elements which were already there + { + constexpr auto assembled_as = x4::as((+alpha).on_match([](auto&& ctx) { + x4::_as_var(ctx) = x4::_attr(ctx); + })); + X4_TEST_SUCCESS("poison"s, "ab cd", assembled_word >> lit(' ') >> assembled_word, "abcd"s); + X4_TEST_SUCCESS("poison"s, "ab cd", assembled_as >> lit(' ') >> assembled_as, "abcd"s); + } + + // An action on `as` passes the value of `as` on, in a sequence into a container too + X4_TEST_SUCCESS("poison"s, "", lit('<') >> x4::as(+alpha).on_match([] {}) >> lit('>'), "ab"s); + + // A rule or `as` of another type passes its value by the ordinary conversion; the result is + // appended to the elements which were already there, the same as it is written into an empty one + { + constexpr auto letters_as = x4::as(+alpha); + constexpr auto shouted_as = x4::as(+alpha); + X4_TEST_SUCCESS("poison"s, "x-ab", alpha >> lit('-') >> letters_word, "xab"s); + X4_TEST_SUCCESS("poison"s, "x-ab", alpha >> lit('-') >> letters_as, "xab"s); + + X4_TEST_SUCCESS("poison"s, "ab", shouted_word, "AB"s); + X4_TEST_SUCCESS("poison"s, "-ab", lit('-') >> shouted_word, "AB"s); + X4_TEST_SUCCESS("poison"s, "x-ab", alpha >> lit('-') >> shouted_word, "xAB"s); + X4_TEST_SUCCESS("poison"s, "ab", shouted_as, "AB"s); + X4_TEST_SUCCESS("poison"s, "-ab", lit('-') >> shouted_as, "AB"s); + X4_TEST_SUCCESS("poison"s, "x-ab", alpha >> lit('-') >> shouted_as, "xAB"s); + } + // The successful branch / subject appends to the elements which were already there X4_TEST_SUCCESS("poison"s, "xab", alpha >> (alpha >> digit | alpha) >> lit('b'), "xa"s); X4_TEST_SUCCESS("poison"s, "xa1", alpha >> (alpha >> digit | alpha), "xa1"s); @@ -274,3 +372,26 @@ TEST_CASE("attribute contract: parser depending on the previous result of the su X4_TEST_SUCCESS(std::vector({7, 8, 9}), "1,2!", +(int_ >> lit(',')) >> -(int_ >> lit('!')), std::vector({1, 2})); X4_TEST_SUCCESS(std::vector({7, 8, 9}), "1,2!", +(int_ >> lit(',')) >> (int_ >> lit('?') | int_ >> lit('!')), std::vector({1, 2})); } + +// Accumulates the digits directly into its attribute, relying on the attribute being prepared to `int{}` +struct accumulating_digits : x4::parser +{ + using attribute_type = int; + + template Se, class Context, x4::X4Attribute Attr> + [[nodiscard]] static constexpr bool parse(It& first, Se const& last, Context const&, Attr& attr) + { + if (first == last || *first < '0' || '9' < *first) return false; + for (; first != last && '0' <= *first && *first <= '9'; ++first) { + attr = attr * 10 + (*first - '0'); + } + return true; + } +}; + +TEST_CASE("attribute contract: plain attribute parser with potential leftover") +{ + constexpr accumulating_digits accumulate{}; + X4_TEST_SUCCESS(999, "12", accumulate, 12); + X4_TEST_SUCCESS(0, "12", (accumulate >> lit(';')) | accumulate, 12); +} diff --git a/test/x4/attribute_type_check.cpp b/test/x4/attribute_type_check.cpp index 98fddced4..15214ca59 100644 --- a/test/x4/attribute_type_check.cpp +++ b/test/x4/attribute_type_check.cpp @@ -37,7 +37,7 @@ struct checked_fixed_value_parser : x4::fixed_value_parser [[nodiscard]] constexpr bool parse(It& first, Se const& last, Context const& ctx, Attr& attr_) const { - static_assert(std::same_as>); + static_assert(std::same_as); return base_type::parse(first, last, ctx, attr_); } }; diff --git a/test/x4/bool.cpp b/test/x4/bool.cpp index 12b2fb927..c5a878f13 100644 --- a/test/x4/bool.cpp +++ b/test/x4/bool.cpp @@ -30,7 +30,7 @@ struct backwards_bool_policies : x4::bool_policies<> using namespace std::string_view_literals; namespace x4 = iris::x4; if (x4::detail::string_parse("eurt"sv, first, last, unused_container, case_compare)) { - x4::move_to(false, attr); // result is false + x4::write_attribute(attr, false); // result is false return true; } return false; diff --git a/test/x4/char.cpp b/test/x4/char.cpp index 7f15d2ed3..85be72e04 100644 --- a/test/x4/char.cpp +++ b/test/x4/char.cpp @@ -26,11 +26,9 @@ TEST_CASE("char") { - static_assert(x4::traits::is_container_v); static_assert(x4::traits::X4Container); static_assert(x4::CategorizedAttr); - static_assert(!x4::traits::is_container_v); static_assert(!x4::traits::X4Container); static_assert(x4::CategorizedAttr); diff --git a/test/x4/container_support.cpp b/test/x4/container_support.cpp index 9a1df2063..f7dd919ac 100644 --- a/test/x4/container_support.cpp +++ b/test/x4/container_support.cpp @@ -15,12 +15,16 @@ #include #include #include +#include #include #include #include #include +#include #include +#include +#include #include #include @@ -30,10 +34,45 @@ #include #include #include +#include #include namespace x4 = iris::x4; +namespace { + +using char_pair = iris::alloy::tuple; +using char_pairs_parser = std::remove_const_t> x4::standard::char_))>; + +// assignable from the whole attribute of `char_pairs_parser` and from `char`, holding neither by its shape +struct converted +{ + int from_pairs = 0; + int from_char = 0; + + converted& operator=(std::vector const&) { ++from_pairs; return *this; } + converted& operator=(char) { ++from_char; return *this; } +}; + +template +constexpr x4::detail::container_parse_strategy char_pairs_parse_for = x4::detail::container_parse_strategy_for; + +// made from a `char` by a converting constructor, with or without a default constructor +struct from_char +{ + from_char() = default; + from_char(char c) : c(c) {} // NOLINT(misc-explicit-constructor) + char c = 0; +}; + +struct from_char_only +{ + from_char_only(char c) : c(c) {} // NOLINT(misc-explicit-constructor) + char c; +}; + +} // anonymous + constexpr x4::rule> pair_rule("pair"); constexpr x4::rule string_rule("string"); @@ -324,72 +363,25 @@ void test_string_support() TEST_CASE("container_support") { - using x4::traits::is_container_v; - using x4::traits::is_associative_v; + using x4::traits::X4Container; // ------------------------------------------------------------------ - STATIC_CHECK(is_container_v); - STATIC_CHECK(!is_associative_v); - STATIC_CHECK(is_container_v>); - STATIC_CHECK(!is_associative_v>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(!is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(!is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(!is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(!is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(!is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(!is_associative_v>>); + STATIC_CHECK(X4Container); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); // ------------------------------------------------------------------ - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); - - STATIC_CHECK(is_container_v>); - STATIC_CHECK(is_associative_v>); - STATIC_CHECK(is_container_v>>); - STATIC_CHECK(is_associative_v>>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); + STATIC_CHECK(X4Container>); // ------------------------------------------------------------------ @@ -410,4 +402,52 @@ TEST_CASE("container_support") test_multimap_support>(); test_multimap_support>(); + + { + // the container held by a variant is appended to, not replaced + constexpr std::string_view input = "cd"; + iris::rvariant v = std::string("ab"); + + auto first = input.begin(); + REQUIRE(x4::detail::parse_into_container(x4::standard::char_, first, input.end(), x4::unused, v)); + REQUIRE(x4::detail::parse_into_container(x4::standard::char_, first, input.end(), x4::unused, v)); + CHECK(first == input.end()); + CHECK(v == iris::rvariant{std::string("abcd")}); + } +} + +TEST_CASE("container_parse_strategy_for") +{ + using x4::detail::container_parse_strategy; + using iris::rvariant; + + STATIC_CHECK(char_pairs_parse_for == container_parse_strategy::container_itself); + STATIC_CHECK(char_pairs_parse_for>> == container_parse_strategy::as_part); + STATIC_CHECK(char_pairs_parse_for>>> == container_parse_strategy::as_part); + STATIC_CHECK(char_pairs_parse_for>>> == container_parse_strategy::as_part); + + // A new plain element is constructed from the value, never constructed by default and assigned + STATIC_CHECK(char_pairs_parse_for> == container_parse_strategy::none); + using char_parser_type = std::remove_const_t; + STATIC_CHECK(x4::detail::container_parse_strategy_for> == container_parse_strategy::as_part); + STATIC_CHECK(x4::detail::container_parse_strategy_for> == container_parse_strategy::as_part); + + constexpr auto char_pairs = x4::eps >> +(x4::standard::char_ >> x4::standard::char_); + { + std::string s; + REQUIRE(parse("abcd", char_pairs, s)); + CHECK(s == "abcd"); + } + { + std::vector>> v; + REQUIRE(parse("abcd", char_pairs, v)); + REQUIRE(v.size() == 1); + CHECK(iris::get>(v[0]).size() == 2); + } + { + std::vector>> v; + REQUIRE(parse("abcd", char_pairs, v)); + REQUIRE(v.size() == 1); + CHECK(iris::get>(v[0]).size() == 2); + } } diff --git a/test/x4/cpp.hint b/test/x4/cpp.hint new file mode 100644 index 000000000..2b48ac7d4 --- /dev/null +++ b/test/x4/cpp.hint @@ -0,0 +1,21 @@ +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +// Hint files help the Visual Studio IDE interpret Visual C++ identifiers +// such as names of functions and macros. +// For more information see https://go.microsoft.com/fwlink/?linkid=865984 + +#define TEST_CASE void test_case() +#define TEMPLATE_TEST_CASE void test_case() +#define CHECK +#define REQUIRE +#define REQUIRE_FALSE +#define REQUIRE_THROWS_AS +#define STATIC_CHECK +#define STATIC_REQUIRE +#define CAPTURE +#define SECTION diff --git a/test/x4/default_error_handler.cpp b/test/x4/default_error_handler.cpp index 4c05f309f..c0e21390e 100644 --- a/test/x4/default_error_handler.cpp +++ b/test/x4/default_error_handler.cpp @@ -6,6 +6,7 @@ =============================================================================*/ #include "iris_x4_test.hpp" +#include "iris_x4_test_parse_debug.hpp" #include @@ -88,6 +89,15 @@ TEST_CASE("print_chars") x4::print_chars(oss, unicode_model_string.begin(), unicode_model_string.end(), 17); CHECK(oss.str() == R"(aこれは𩸽だ サロゲート\tペア\u{1680}入)"); } + + { + std::ostringstream oss; + oss.width(8); + oss.fill('*'); + x4::print_chars(oss, U'\u1680'); + CHECK(oss.str() == R"(\u{1680})"); + CHECK(oss.width() == 8); + } } TEST_CASE("print_line_highlight") diff --git a/test/x4/difference.cpp b/test/x4/difference.cpp index 1899f954c..56d2a8f13 100644 --- a/test/x4/difference.cpp +++ b/test/x4/difference.cpp @@ -12,7 +12,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -23,6 +26,7 @@ TEST_CASE("difference") { using x4::standard::char_; + using x4::standard::alpha; using x4::standard::blank; using x4::standard::space; using x4::lit; @@ -86,4 +90,35 @@ TEST_CASE("difference") )); CHECK(s == "foo die"); // wrong implementation yields "foodie" } + + // The container is passed to `Left`, which appends each word into the string itself + { + constexpr auto words = +alpha % ' '; + STATIC_CHECK(x4::parser_traits::accepts_container); + + std::string text; + REQUIRE(parse("ab cd", -(words - lit('x')), text)); + CHECK(text == "abcd"); + + // What `Left` appended before it failed is not left after an optional or an alternative + // recovers; a repetition keeps the preceding ones + { + auto const res = parse("ab cd!", -((words >> ';') - lit('x')), text); + REQUIRE(res.ok); + CHECK(text.empty()); + CHECK(res.remainder_str() == "ab cd!"); + } + { + auto const res = parse("ab!", ((words >> ';') - lit('x')) | +alpha, text); + REQUIRE(res.ok); + CHECK(text == "ab"); + CHECK(res.remainder_str() == "!"); + } + { + auto const res = parse("ab;cd ef!", *((words >> ';') - lit('x')), text); + REQUIRE(res.ok); + CHECK(text == "ab"); + CHECK(res.remainder_str() == "cd ef!"); + } + } } diff --git a/test/x4/iris_x4_test.hpp b/test/x4/iris_x4_test.hpp index ede8c2b4f..18de76e83 100644 --- a/test/x4/iris_x4_test.hpp +++ b/test/x4/iris_x4_test.hpp @@ -14,9 +14,8 @@ #include #include -#include +#include #include -#include #include // IWYU pragma: export @@ -24,7 +23,6 @@ #include #include #include -#include namespace alloy = iris::alloy; namespace x4 = iris::x4; @@ -116,90 +114,11 @@ struct parse_overloads : x4::detail::parse_fn } }; // parse_overload -// Provide `x4::unused` default arg fallback -struct parse_debug_overloads : x4::detail::parse_debug_fn_main -{ - using x4::detail::parse_debug_fn_main::operator(); - - // It/Se + Parser - template Se, x4::X4Parser Parser> - static constexpr x4::parse_result - operator()(It first, Se last, Parser&& p) - { - return x4::parse_debug(first, last, std::forward(p), x4::unused); - } - - // parse_result + It/Se + Parser - template Se, x4::X4Parser Parser> - static constexpr void - operator()(x4::parse_result& res, It first, Se last, Parser&& p) - { - return x4::parse_debug(res, first, last, std::forward(p), x4::unused); - } - - // R + Parser - template Parser> - static constexpr x4::parse_result_for - operator()(R const& r, Parser&& p) - { - return x4::parse_debug(r, std::forward(p), x4::unused); - } - - // parse_result + R + Parser - template Parser> - static constexpr void - operator()(x4::parse_result_for& res, R const& r, Parser&& p) - { - return x4::parse_debug(res, r, std::forward(p), x4::unused); - } - - // It/Se + Parser + Skipper - template Se, x4::X4Parser Parser, x4::X4ExplicitParser Skipper> - static constexpr x4::parse_result - operator()(It first, Se last, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) - { - return x4::parse_debug(first, last, std::forward(p), std::forward(s), x4::unused, flag); - } - - // parse_result + It/Se + Parser + Skipper - template Se, x4::X4Parser Parser, x4::X4ExplicitParser Skipper> - static constexpr void - operator()(x4::parse_result& res, It first, Se last, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) - { - return x4::parse_debug(res, first, last, std::forward(p), std::forward(s), x4::unused, flag); - } - - // R + Parser + Skipper - template< - std::ranges::forward_range R, - x4::detail::X4RangeParseParser Parser, - x4::detail::X4RangeParseSkipper Skipper - > - static constexpr x4::parse_result_for - operator()(R const& r, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) - { - return x4::parse_debug(r, std::forward(p), std::forward(s), x4::unused, flag); - } - - // parse_result + R + Parser + Skipper - template< - std::ranges::forward_range R, - x4::detail::X4RangeParseParser Parser, - x4::detail::X4RangeParseSkipper Skipper - > - static constexpr void - operator()(x4::parse_result_for& res, R const& r, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) - { - return x4::parse_debug(res, r, std::forward(p), std::forward(s), x4::unused, flag); - } -}; // parse_overload - } // detail inline namespace cpos { [[maybe_unused]] inline constexpr detail::parse_overloads parse{}; -[[maybe_unused]] inline constexpr detail::parse_debug_overloads parse_debug{}; } // cpos @@ -244,7 +163,7 @@ struct synth_parser : x4::parser> { if (iter != last && *iter == 's') { ++iter; - x4::move_to(attribute_type{}, attr); + x4::write_attribute(attr, attribute_type{}); return true; } return false; @@ -272,7 +191,6 @@ struct custom_container } // x4_test using x4_test::parse; -using x4_test::parse_debug; #define IRIS_X4_ASSERT_CONSTEXPR_CTORS(...) \ static_assert(::x4_test::test_constexpr_copy_move_ctors(__VA_ARGS__)) diff --git a/test/x4/iris_x4_test_parse_debug.hpp b/test/x4/iris_x4_test_parse_debug.hpp new file mode 100644 index 000000000..93411accf --- /dev/null +++ b/test/x4/iris_x4_test_parse_debug.hpp @@ -0,0 +1,113 @@ +#ifndef IRIS_ZZ_X4_TEST_IRIS_X4_TEST_PARSE_DEBUG_HPP +#define IRIS_ZZ_X4_TEST_IRIS_X4_TEST_PARSE_DEBUG_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +#include "iris_x4_test.hpp" + +#include + +#include +#include +#include + +namespace x4_test { + +namespace detail { + +// Provide `x4::unused` default arg fallback +struct parse_debug_overloads : x4::detail::parse_debug_fn_main +{ + using x4::detail::parse_debug_fn_main::operator(); + + // It/Se + Parser + template Se, x4::X4Parser Parser> + static constexpr x4::parse_result + operator()(It first, Se last, Parser&& p) + { + return x4::parse_debug(first, last, std::forward(p), x4::unused); + } + + // parse_result + It/Se + Parser + template Se, x4::X4Parser Parser> + static constexpr void + operator()(x4::parse_result& res, It first, Se last, Parser&& p) + { + return x4::parse_debug(res, first, last, std::forward(p), x4::unused); + } + + // R + Parser + template Parser> + static constexpr x4::parse_result_for + operator()(R const& r, Parser&& p) + { + return x4::parse_debug(r, std::forward(p), x4::unused); + } + + // parse_result + R + Parser + template Parser> + static constexpr void + operator()(x4::parse_result_for& res, R const& r, Parser&& p) + { + return x4::parse_debug(res, r, std::forward(p), x4::unused); + } + + // It/Se + Parser + Skipper + template Se, x4::X4Parser Parser, x4::X4ExplicitParser Skipper> + static constexpr x4::parse_result + operator()(It first, Se last, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) + { + return x4::parse_debug(first, last, std::forward(p), std::forward(s), x4::unused, flag); + } + + // parse_result + It/Se + Parser + Skipper + template Se, x4::X4Parser Parser, x4::X4ExplicitParser Skipper> + static constexpr void + operator()(x4::parse_result& res, It first, Se last, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) + { + return x4::parse_debug(res, first, last, std::forward(p), std::forward(s), x4::unused, flag); + } + + // R + Parser + Skipper + template< + std::ranges::forward_range R, + x4::detail::X4RangeParseParser Parser, + x4::detail::X4RangeParseSkipper Skipper + > + static constexpr x4::parse_result_for + operator()(R const& r, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) + { + return x4::parse_debug(r, std::forward(p), std::forward(s), x4::unused, flag); + } + + // parse_result + R + Parser + Skipper + template< + std::ranges::forward_range R, + x4::detail::X4RangeParseParser Parser, + x4::detail::X4RangeParseSkipper Skipper + > + static constexpr void + operator()(x4::parse_result_for& res, R const& r, Parser&& p, Skipper&& s, x4::root_skipper_flag flag = x4::root_skipper_flag::do_post_skip) + { + return x4::parse_debug(res, r, std::forward(p), std::forward(s), x4::unused, flag); + } +}; // parse_overload + +} // detail + +inline namespace cpos { + +[[maybe_unused]] inline constexpr detail::parse_debug_overloads parse_debug{}; + +} // cpos + +} // x4_test + +using x4_test::parse_debug; + +#endif diff --git a/test/x4/iterator.cpp b/test/x4/iterator.cpp index d837bb498..d3f4e196f 100644 --- a/test/x4/iterator.cpp +++ b/test/x4/iterator.cpp @@ -61,12 +61,8 @@ using namespace std::string_view_literals; // On failed parse, // - The `first` iterator shall point to the previous position, and -// - The attribute shall not be modified. -// - Best-effort; there are many exceptions as of now. In other words, -// rollbacks may or may not occur depending on the situation. -// - In practice, the current implementation only "holds" the attribute -// only when it is semantically possible to preliminarily check the -// parse success on the right hand side of the underlying parser. +// - The attribute is left in a valid but unspecified state when `parse` of a parser is +// called directly, as the tests below do (`x4::parse` resets it to its default state). // NOLINTBEGIN(readability-container-size-empty) diff --git a/test/x4/kleene.cpp b/test/x4/kleene.cpp index a86e37f0b..5d0d264c7 100644 --- a/test/x4/kleene.cpp +++ b/test/x4/kleene.cpp @@ -14,18 +14,25 @@ #include #include #include +#include #include +#include +#include +#include + +#include #include +#include #include TEST_CASE("kleene") { using x4::char_; - using x4::alpha; - using x4::upper; - using x4::space; - using x4::digit; + using x4::standard::alpha; + using x4::standard::upper; + using x4::standard::space; + using x4::standard::digit; using x4::int_; using x4::lexeme; @@ -119,4 +126,77 @@ TEST_CASE("kleene") REQUIRE(parse("sss", *x4_test::synth_move_only, v)); CHECK(v.size() == 3); } + + // the whole value is one element when the element type takes it, else each parse is written as a part + { + std::vector v; + REQUIRE(parse("abc1", *~char_(','), v)); + CHECK(v == std::vector{"abc1"}); + } + { + std::vector v; + REQUIRE(parse("a1b2", *(alpha >> digit), v)); + CHECK(v == std::vector{"a1b2"}); + } + { + std::vector> v; + REQUIRE(parse("1a2b", *(int_ >> alpha), v)); + CHECK(v == std::vector>{{1, 'a'}, {2, 'b'}}); + } + { + std::vector v; + REQUIRE(parse("1,2", *(int_ >> ',' >> int_), v)); + CHECK(v == std::vector{1, 2}); + } + { + constexpr auto items = *~char_(',') >> *(',' >> *~char_(',')); + std::vector v; + REQUIRE(parse("abc1,abc2", items, v)); + CHECK(v == std::vector{"abc1", "abc2"}); + + std::string s; + REQUIRE(parse("abc1,abc2", items, s)); + CHECK(s == "abc1abc2"); + } + { + std::vector v; + REQUIRE(parse("ab", +~char_(','), v)); + CHECK(v == std::vector{"ab"}); + } + { + // An optional from one parse is appended as is if the element type accepts it, else only its content + std::vector v; + REQUIRE(parse("a,,b,", *(-alpha >> ','), v)); + CHECK(v == std::vector{'a', 'b'}); + + std::vector> o; + REQUIRE(parse("a,,b,", *(-alpha >> ','), o)); + CHECK(o == std::vector>{'a', std::nullopt, 'b'}); + } + { + // The container held by a variant is appended to, not replaced + constexpr std::string_view input = "cd"; + iris::rvariant v = std::string("ab"); + + auto first = input.begin(); + REQUIRE((*char_).parse(first, input.end(), x4::unused, v)); + CHECK(first == input.end()); + CHECK(v == iris::rvariant{std::string("abcd")}); + } + { + // The container in an optional is engaged and appended to, as a value is written into an optional + iris::alloy::tuple> t; + REQUIRE(parse("x:ab", char_ >> ':' >> *alpha, t)); + CHECK(t == iris::alloy::tuple>{'x', std::string("ab")}); + + iris::rvariant> v; + REQUIRE(parse("ab", *alpha, v)); + CHECK(v == iris::rvariant>{std::optional("ab")}); + + constexpr std::string_view input = "cd"; + std::optional o = std::string("ab"); + auto first = input.begin(); + REQUIRE((*alpha).parse(first, input.end(), x4::unused, o)); + CHECK(o == std::string("abcd")); + } } diff --git a/test/x4/lit.cpp b/test/x4/lit.cpp index 03cece7f3..107033cbc 100644 --- a/test/x4/lit.cpp +++ b/test/x4/lit.cpp @@ -19,6 +19,7 @@ #include #include +#include TEST_CASE("lit") { @@ -137,4 +138,15 @@ TEST_CASE("lit") REQUIRE(parse("kimpo", x4::string("kimpo"), s)); CHECK(alloy::get<0>(s) == "kimpo"); } + + { + constexpr std::string_view input = "ab"; + std::string s = "z"; + + auto first = input.begin(); + REQUIRE(x4::lit('a').parse(first, input.end(), x4::unused, s)); + REQUIRE(x4::lit("b").parse(first, input.end(), x4::unused, s)); + CHECK(first == input.end()); + CHECK(s == "z"); + } } diff --git a/test/x4/move_to.cpp b/test/x4/move_to.cpp deleted file mode 100644 index 80cdbb882..000000000 --- a/test/x4/move_to.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "iris_x4_test.hpp" - -#include - -#include -#include -#include -#include - -#include - -struct X {}; -struct Y {}; -struct Z {}; - -struct Xs -{ - std::vector xs; -}; -IRIS_ALLOY_ADAPT_STRUCT(Xs, xs); - -using XYZ = iris::rvariant; - - -TEST_CASE("move_to") -{ - // TODO: add more test - - // Currently not permitted by design, but may be changed in the future - // if legitimate rationale is discovered - { - //XYZ xyz; - //xyz = std::vector{}; - static_assert(!std::is_assignable_v>); - } - - // tuple contains reference - { - int n = 0; - alloy::tuple ref_tuple{ n }; - x4::move_to(42, ref_tuple); - CHECK(n == 42); - } - { - int n = 42; - alloy::tuple ref_tuple{ n }; - alloy::tuple dest{ 0 }; - x4::move_to(std::move(ref_tuple), dest); - CHECK(alloy::get<0>(dest) == 42); - } - { - x4_test::move_only mo; - alloy::tuple ref_tuple{ mo }; - alloy::tuple dest; - x4::move_to(std::move(ref_tuple), dest); // move "far" ownership - } -} diff --git a/test/x4/optional.cpp b/test/x4/optional.cpp index acbbc1798..64bf0a9e9 100644 --- a/test/x4/optional.cpp +++ b/test/x4/optional.cpp @@ -10,6 +10,7 @@ #include "iris_x4_test.hpp" #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #ifdef _MSC_VER // bogus https://developercommunity.visualstudio.com/t/buggy-warning-c4709/471956 @@ -200,4 +203,31 @@ TEST_CASE("optional") CHECK(s == "x"); } } + + // Optional into container + { + std::vector> v; + REQUIRE(parse("a", char_ >> -char_, v)); + CHECK(v == std::vector>{'a', std::nullopt}); + } + { + std::vector v; + REQUIRE(parse("a", char_ >> -char_, v)); + CHECK(v == std::vector{'a'}); + } + { + std::vector v; + REQUIRE(parse("a", char_ >> -char_, v)); + CHECK(v == std::vector{97}); // no 0 for the value absent + } + { + std::vector v; + REQUIRE(parse("#a,#", ('#' >> -x4::standard::alpha) % ',', v)); + CHECK(v == std::vector{'a'}); + } + { + char c = 0; + REQUIRE(parse(",c", -(',' >> char_), c)); + CHECK(c == 'c'); + } } diff --git a/test/x4/parser.cpp b/test/x4/parser.cpp index 0ed6165bf..0b0d86608 100644 --- a/test/x4/parser.cpp +++ b/test/x4/parser.cpp @@ -95,7 +95,7 @@ struct minimal_binary_unused_parser TEST_CASE("parser") { - using x4::blank; + using x4::standard::blank; { using It = std::string_view::const_iterator; diff --git a/test/x4/partial_success.cpp b/test/x4/partial_success.cpp index 843877ee3..f09489ee9 100644 --- a/test/x4/partial_success.cpp +++ b/test/x4/partial_success.cpp @@ -97,7 +97,6 @@ TEST_CASE("partial success (alternative)") { static_assert(x4::CategorizedAttr, x4::container_tag>); static_assert(x4::traits::X4Container>); - static_assert(x4::traits::is_container_v>); { std::vector ints; @@ -111,7 +110,7 @@ TEST_CASE("partial success (alternative)") // `x4::alternative`, we would see {1, 2, 98, 99} here. CHECK(ints == std::vector{98, 99}); } - // Failed parse should not modify the exposed attribute + // A failed parse leaves the exposed attribute in its default state { std::vector ints; REQUIRE(!parse("1 2", int_ >> int_ >> eps(false) | fixed_value(98) >> fixed_value(99) >> eps(false), space, ints)); @@ -131,7 +130,6 @@ TEST_CASE("partial success (alternative)") { static_assert(x4::CategorizedAttr); static_assert(x4::traits::X4Container); - static_assert(x4::traits::is_container_v); { std::string str; @@ -161,7 +159,7 @@ TEST_CASE("partial success (alternative)") // Wrong implementation yields "bookwormfoodie" CHECK(str == "foodie"); } - // Failed parse should not modify the exposed attribute + // A failed parse leaves the exposed attribute in its default state { std::string str; REQUIRE(!parse("foodie", fixed_value("bookworm") >> eps(false) | string("foodie") >> eps(false), str)); @@ -181,7 +179,6 @@ TEST_CASE("partial success (alternative)") { static_assert(x4::CategorizedAttr); static_assert(!x4::traits::X4Container); - static_assert(!x4::traits::is_container_v); { strong_int si; @@ -201,7 +198,6 @@ TEST_CASE("partial success (alternative)") static_assert(x4::CategorizedAttr); static_assert(!x4::traits::X4Container); - static_assert(!x4::traits::is_container_v); { pair_int pi; @@ -245,14 +241,14 @@ TEST_CASE("partial success (list-like)") >; static_assert(std::same_as, Subject>); STATIC_CHECK(std::same_as::attribute_type, alloy::tuple>); - STATIC_CHECK(x4::detail::container_can_hold_sequence>::value); + STATIC_CHECK(x4::planner::node_write_strategy_of>.kind == x4::planner::branch_kind::new_element); using Container = std::string; - STATIC_CHECK(x4::parser_traits::template handles_container); - STATIC_CHECK(x4::parser_traits>::template handles_container); - STATIC_CHECK(x4::parser_traits>::template handles_container); - STATIC_CHECK(x4::parser_traits>>::template handles_container); + STATIC_CHECK(x4::parser_traits::template accepts_container); + STATIC_CHECK(x4::parser_traits>::template accepts_container); + STATIC_CHECK(x4::parser_traits>::template accepts_container); + STATIC_CHECK(x4::parser_traits>>::template accepts_container); } // kleene @@ -337,14 +333,15 @@ TEST_CASE("partial success (list-like)") >; static_assert(std::same_as, Subject>); STATIC_CHECK(std::same_as::attribute_type, alloy::tuple>); - STATIC_CHECK(x4::detail::container_can_hold_sequence>::value); + STATIC_CHECK(x4::planner::node_write_strategy_of>.kind == x4::planner::branch_kind::new_element); + STATIC_CHECK(x4::write_rank_v == x4::write_rank::structural); using Container = std::string; - STATIC_CHECK(x4::parser_traits::template handles_container); - STATIC_CHECK(x4::parser_traits>::template handles_container); - STATIC_CHECK(x4::parser_traits>::template handles_container); - STATIC_CHECK(x4::parser_traits>>::template handles_container); + STATIC_CHECK(x4::parser_traits::template accepts_container); + STATIC_CHECK(x4::parser_traits>::template accepts_container); + STATIC_CHECK(x4::parser_traits>::template accepts_container); + STATIC_CHECK(x4::parser_traits>>::template accepts_container); } // kleene diff --git a/test/x4/pch.cpp b/test/x4/pch.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/test/x4/pch.hpp b/test/x4/pch.hpp new file mode 100644 index 000000000..f85131723 --- /dev/null +++ b/test/x4/pch.hpp @@ -0,0 +1,43 @@ +#ifndef IRIS_ZZ_X4_TEST_PCH_HPP +#define IRIS_ZZ_X4_TEST_PCH_HPP + +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/test/x4/plus.cpp b/test/x4/plus.cpp index 8b792f198..71796c10c 100644 --- a/test/x4/plus.cpp +++ b/test/x4/plus.cpp @@ -26,10 +26,10 @@ TEST_CASE("plus") { using x4::char_; - using x4::alpha; - using x4::upper; - using x4::space; - using x4::digit; + using x4::standard::alpha; + using x4::standard::upper; + using x4::standard::space; + using x4::standard::digit; using x4::no_case; using x4::int_; using x4::omit; diff --git a/test/x4/repeat.cpp b/test/x4/repeat.cpp index 59b047758..6963984b4 100644 --- a/test/x4/repeat.cpp +++ b/test/x4/repeat.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include #include @@ -180,4 +182,27 @@ TEST_CASE("repeat") REQUIRE(parse("sss", repeat(3)[x4_test::synth_move_only], v)); CHECK(v.size() == 3); } + + { + constexpr auto words = +alpha % ' '; + STATIC_CHECK(x4::parser_traits::accepts_container); + STATIC_CHECK(x4::parser_traits::accepts_container); + + std::string text; + REQUIRE(parse("ab cd", -repeat(1)[words], text)); + CHECK(text == "abcd"); + + { + auto const res = parse("ab;cd", -repeat(2)[words >> ';'], text); + REQUIRE(res.ok); + CHECK(text.empty()); + CHECK(res.remainder_str() == "ab;cd"); + } + { + auto const res = parse("ab;cd ef", *repeat(1)[words >> ';'], text); + REQUIRE(res.ok); + CHECK(text == "ab"); + CHECK(res.remainder_str() == "cd ef"); + } + } } diff --git a/test/x4/rule1.cpp b/test/x4/rule1.cpp index 25edbfd8b..4ef91a9e7 100644 --- a/test/x4/rule1.cpp +++ b/test/x4/rule1.cpp @@ -50,12 +50,12 @@ TEST_CASE("rule1") static_assert( x4::has_attribute_v{} = int_)>); static_assert(!x4::has_attribute_v{} = int_)>); - STATIC_CHECK( x4::parser_traits>::handles_container); - STATIC_CHECK( x4::parser_traits>>::handles_container>); - //STATIC_CHECK( x4::parser_traits>>::handles_container>>); - STATIC_CHECK(!x4::parser_traits>::handles_container>); // yields an element, not the container - //STATIC_CHECK(!x4::parser_traits>>::handles_container); // yields a variant, cannot write into `std::string` - STATIC_CHECK(!x4::parser_traits>>::handles_container>>); + STATIC_CHECK( x4::parser_traits>::accepts_container); + STATIC_CHECK( x4::parser_traits>>::accepts_container>); + //STATIC_CHECK( x4::parser_traits>>::accepts_container>>); + STATIC_CHECK(!x4::parser_traits>::accepts_container>); // yields an element, not the container + //STATIC_CHECK(!x4::parser_traits>>::accepts_container); // yields a variant, cannot write into `std::string` + STATIC_CHECK(!x4::parser_traits>>::accepts_container>>); { // basic tests diff --git a/test/x4/rule_separate_tu_grammar.hpp b/test/x4/rule_separate_tu_grammar.hpp index 580d30f24..3f1eeabe1 100644 --- a/test/x4/rule_separate_tu_grammar.hpp +++ b/test/x4/rule_separate_tu_grammar.hpp @@ -24,8 +24,8 @@ using skipper_type = x4::rule; constexpr skipper_type skipper = "skipper"; IRIS_X4_DECLARE(skipper_type) -// the `unused_type const` must have the same effect as no attribute -using skipper2_type = x4::rule; +// an explicit `unused_type` must have the same effect as no attribute +using skipper2_type = x4::rule; constexpr skipper2_type skipper2 = "skipper2"; IRIS_X4_DECLARE(skipper2_type) diff --git a/test/x4/sequence.cpp b/test/x4/sequence.cpp index 92e6dbf66..35541676a 100644 --- a/test/x4/sequence.cpp +++ b/test/x4/sequence.cpp @@ -38,6 +38,8 @@ #include #include +// NOLINTBEGIN(bugprone-chained-comparison) + TEST_CASE("sequence") { namespace traits = x4::traits; @@ -507,39 +509,58 @@ TEST_CASE("sequence") typename T::value_type; }); STATIC_CHECK(requires(T& c) { - traits::begin(c); + std::ranges::begin(c); }); STATIC_CHECK(requires(T& c) { - requires std::forward_iterator; + requires std::forward_iterator; }); STATIC_CHECK(requires(T& c) { - traits::end(c); + std::ranges::end(c); }); STATIC_CHECK(requires(T& c) { - requires std::sentinel_for; + requires std::sentinel_for; }); STATIC_CHECK(requires(T& c) { - traits::is_empty(c); + std::ranges::empty(c); }); STATIC_CHECK(requires(T& c) { - traits::push_back(c, std::declval()); + iris::container::append(c, std::declval()); }); - STATIC_CHECK(requires(T& c) { - traits::append( - c, - std::declval(), - std::declval() - ); + STATIC_CHECK(requires(T& c, T& other) { + iris::container::append_range(c, other | std::views::as_rvalue); }); STATIC_CHECK(requires(T& c) { - traits::clear(c); + iris::container::clear(c); }); - STATIC_CHECK(traits::is_container_v>); + STATIC_CHECK(traits::X4Container>); STATIC_CHECK(x4::CategorizedAttr, x4::container_tag>); std::vector v; REQUIRE(parse("ssszs", *x4_test::synth_move_only >> 'z' >> x4_test::synth_move_only, v)); CHECK(v.size() == 4); } + + // Each part of a sequence that is not a range is appended as a new element, even if it is the + // only part with a value + { + std::string s; + REQUIRE(parse("a\n", x4::standard::char_ >> '\n', s)); + CHECK(s == "a"); + } + { + constexpr x4::rule r = "r"; + (void)r; + constexpr auto char_opt_char2 = r = x4::standard::char_ >> -(x4::standard::char_ >> x4::standard::char_); + + std::string s; + REQUIRE(parse("xa1", char_opt_char2, s)); + CHECK(s == "xa1"); + + auto const res = parse("xa", char_opt_char2, s); + REQUIRE(res.is_partial_match()); + CHECK(s == "x"); + } } + +// NOLINTEND(bugprone-chained-comparison) diff --git a/test/x4/smart_ptr.cpp b/test/x4/smart_ptr.cpp index 53e81bf80..8b4a8f2ff 100644 --- a/test/x4/smart_ptr.cpp +++ b/test/x4/smart_ptr.cpp @@ -8,11 +8,17 @@ #include "iris_x4_test.hpp" #include +#include +#include #include #include +#include +#include #include #include +#include +#include template struct throwing_parser : x4::parser> @@ -238,3 +244,28 @@ TEST_CASE("shared_ptr (custom deleter)") REQUIRE(!result); } } + +TEST_CASE("smart_ptr in a container") +{ + using x4::int_; + using x4::standard::alpha; + + STATIC_CHECK(x4::parser_traits::accepts_container); + STATIC_CHECK(!x4::parser_traits::accepts_container); + STATIC_CHECK(!x4::parser_traits::accepts_container); + + { + std::vector> result; + REQUIRE(parse("1,2", x4::unique_ptr(int_) % ',', result)); + REQUIRE(result.size() == 2); + CHECK(*result[0] == 1); + CHECK(*result[1] == 2); + } + { + std::vector> result; + REQUIRE(parse("1,2", x4::shared_ptr(int_) % ',', result)); + REQUIRE(result.size() == 2); + CHECK(*result[0] == 1); + CHECK(*result[1] == 2); + } +} diff --git a/test/x4/substitution.cpp b/test/x4/substitution.cpp deleted file mode 100644 index 24a065f6d..000000000 --- a/test/x4/substitution.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "iris_x4_test.hpp" - -#include -#include - -#include - -#include - -using x4::can_hold_v; - -TEST_CASE("can_hold") -{ - // identical types - STATIC_CHECK(can_hold_v); - STATIC_CHECK(can_hold_v, std::vector>); - STATIC_CHECK(can_hold_v, alloy::tuple>); - STATIC_CHECK(can_hold_v, iris::rvariant>); - - // `iris::rvariant` is "broader" than `int` - STATIC_CHECK( can_hold_v, int>); - STATIC_CHECK(!can_hold_v>); - - // container types - STATIC_CHECK(can_hold_v>, std::vector>); - - // tuple-like types - STATIC_CHECK(can_hold_v>, alloy::tuple>); -} diff --git a/test/x4/unused.cpp b/test/x4/unused.cpp index 204075913..2d14afb71 100644 --- a/test/x4/unused.cpp +++ b/test/x4/unused.cpp @@ -12,7 +12,6 @@ #include #include -#include #include #include @@ -21,8 +20,6 @@ TEST_CASE("unused") { namespace traits = x4::traits; - using x4::Transformable; - using x4::transform_attribute; using x4::unused_container_type; using x4::unused_container; @@ -62,34 +59,4 @@ TEST_CASE("unused") STATIC_CHECK(x4::X4Attribute); STATIC_CHECK(x4::X4Attribute); - - // unused => unused - { - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_type>); - } - - // unused_container => unused_container - { - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_container_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_container_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_container_type>); - - STATIC_CHECK(Transformable); - STATIC_CHECK(std::same_as::type, unused_container_type>); - } } diff --git a/test/x4/value.cpp b/test/x4/value.cpp index 8eb67ea39..eb9782060 100644 --- a/test/x4/value.cpp +++ b/test/x4/value.cpp @@ -22,6 +22,18 @@ #include #include +namespace { + +// The default value owns dynamically allocated storage, so it cannot be held in a `constexpr` variable +struct allocating_default +{ + std::vector values{1, 2, 3}; +}; + +constexpr auto allocating_default_p = x4::default_value; + +} // anonymous + TEST_CASE("attr") { using namespace std::string_literals; @@ -149,7 +161,7 @@ TEST_CASE("attr") { std::vector> vecs; std::vector vec{1, 2, 3}; - x4::move_to(std::move(vec), vecs); + x4::write_attribute(vecs, std::move(vec)); CHECK(vecs == std::vector>{std::vector{1, 2, 3}}); } { @@ -162,7 +174,7 @@ TEST_CASE("attr") { std::vector strs; std::string str = "abc"; - x4::move_to(std::move(str), strs); + x4::write_attribute(strs, std::move(str)); CHECK(strs == std::vector{std::string("abc")}); } { @@ -194,25 +206,34 @@ TEST_CASE("attr") } } -TEST_CASE("reset_value") +TEST_CASE("default_value") { - using x4::reset_value; + using x4::default_value; { int val = 42; - STATIC_CHECK(std::same_as)>::attribute_type, int>); - REQUIRE(parse("", reset_value, val)); + STATIC_CHECK(std::same_as)>::attribute_type, int>); + REQUIRE(parse("", default_value, val)); CHECK(val == 0); } + + // `T{}` written by the ordinary write rules, as `fixed_value` writes its value + STATIC_CHECK(std::same_as>)>::attribute_type, std::vector>); { std::vector val; - val.reserve(100); - val.emplace_back(42); - auto const prev_capacity = val.capacity(); - - STATIC_CHECK(std::same_as>)>::attribute_type, std::vector>); - REQUIRE(parse("", reset_value>, val)); - CHECK(val.empty()); - CHECK(val.capacity() == prev_capacity); // should preserve capacity as per `.clear()` + REQUIRE(parse("1,2", x4::int_ >> ',' >> default_value> >> x4::int_, val)); + CHECK(val == std::vector{1, 2}); + } + { + std::vector> val; + REQUIRE(parse("", default_value>, val)); + CHECK(val == std::vector>{{}}); + } + + // a `constexpr` parser, although the default value owns dynamically allocated storage + { + allocating_default val{{}}; + REQUIRE(parse("", allocating_default_p, val)); + CHECK(val.values == std::vector{1, 2, 3}); } } diff --git a/test/x4/write_attribute.cpp b/test/x4/write_attribute.cpp new file mode 100644 index 000000000..23fdbe85c --- /dev/null +++ b/test/x4/write_attribute.cpp @@ -0,0 +1,807 @@ +/*============================================================================= + Copyright (c) 2026 The Iris Project Contributors + + 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) +=============================================================================*/ + +#include "iris_x4_test.hpp" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using iris::rvariant; +using iris::recursive_wrapper; +using x4::write_rank; + +// the types of the requirements + +struct Port +{ + Port(int value) : value(value) {} + int value; + bool operator==(Port const&) const = default; +}; + +struct Shape +{ + Shape(int) {} + Shape(std::initializer_list) {} +}; + +struct Id +{ + explicit Id(int) {} + Id& operator=(int) { return *this; } +}; + +struct Wrap +{ + template + Wrap(T&& t) : v(t) {} // NOLINT(bugprone-forwarding-reference-overload, cppcoreguidelines-missing-std-forward) + int v; + bool operator==(Wrap const&) const = default; +}; + +struct NoDefault +{ + NoDefault(int value) : value(value) {} + int value; + bool operator==(NoDefault const&) const = default; +}; + +struct Ctor +{ + Ctor(int) {} + Ctor& operator=(int) = delete; +}; + +struct AmbiguousCtor +{ + explicit AmbiguousCtor(long) {} + AmbiguousCtor(unsigned) {} +}; + +struct Span +{ + Span(int first, int count) : first(first), last(first + count) {} + int first, last; + bool operator==(Span const&) const = default; +}; + +struct Paren; +using Expr = rvariant>; + +struct Paren +{ + Expr inner; + bool operator==(Paren const&) const = default; +}; + +struct Ident +{ + std::string name; + bool operator==(Ident const&) const = default; +}; + +struct Assign { Ident lhs; Expr rhs; }; +struct Compare { Ident lhs; Expr rhs; }; + +struct Literal +{ + long long value = 0; + bool operator==(Literal const&) const = default; +}; + +struct Real { double value = 0; }; +struct Count { int value = 0; }; + +struct Ints +{ + std::vector values; + bool operator==(Ints const&) const = default; +}; + +struct ReturnStmt { Expr value; }; +struct ExprStmt { Expr value; }; +struct Number { rvariant value; }; + +struct Point +{ + int x = 0, y = 0; + bool operator==(Point const&) const = default; +}; + +// a single-element tuple-like of a single-element tuple-like of its own value +struct LongLongNumber { long long value = 0; }; +struct VariantNumber { rvariant value; }; + +// recursive types + +struct binop; +using operand = rvariant>; + +struct binop +{ + operand l; + char op = 0; + operand r; + bool operator==(binop const&) const = default; +}; + +struct Tree +{ + int v = 0; + std::vector kids; +}; + +struct Items +{ + std::vector items; +}; + +struct Box; +using boxed = rvariant>>; +struct Box { boxed inner; }; + +struct Single { int n = 0; }; + +struct BoxOrSingle; +using box_or_single = rvariant>, Single>; +struct BoxOrSingle { box_or_single inner; }; + +struct BoxOrOptional; +using box_or_optional = rvariant>, std::optional>; +struct BoxOrOptional { box_or_optional inner; }; + +// each writes an int into the other before its own single-element struct: the selections return +// to each other with the same value, a conflict +struct SingleA { int n = 0; }; +struct SingleB { int n = 0; }; +struct CycleBoxA; +struct CycleBoxB; +using cycle_a = rvariant>, SingleA>; +using cycle_b = rvariant>, SingleB>; +struct CycleBoxA { cycle_a inner; }; +struct CycleBoxB { cycle_b inner; }; +struct HoldsCycleA { cycle_a a; }; + +// agree_b converts before it writes into agree_a: no conflict +struct AgreeBoxA; +struct AgreeBoxB; +using agree_a = rvariant>, SingleA>; +using agree_b = rvariant>, long long>; +struct AgreeBoxA { agree_a inner; }; +struct AgreeBoxB { agree_b inner; }; + +// both optionals of tie_m take an int, one of them through tie_n +struct TieBoxM; +struct TieBoxN; +using tie_n = rvariant>, Single>; +using tie_m = rvariant, std::optional>>; +struct TieBoxM { tie_m inner; }; +struct TieBoxN { tie_n inner; }; + +// X = rvariant>> +struct IntArray; +using int_node = rvariant>; + +struct IntArray : std::vector +{ + using std::vector::vector; +}; + +// X = rvariant>> +struct StringArray; +using string_node = rvariant>; + +struct StringArray : std::vector +{ + using std::vector::vector; +}; + +struct X {}; +struct Y {}; +struct Z {}; +struct Xs { std::vector xs; }; + +// an AST whose statements and expressions recur into each other +namespace ast { + +struct Binary; +struct Unary; +struct Call; +struct Lambda; +struct Name { std::string text; }; +using Expr = rvariant, recursive_wrapper, recursive_wrapper, recursive_wrapper>; +struct Binary { Expr lhs; char op = 0; Expr rhs; }; +struct Unary { char op = 0; Expr operand; }; +struct Call { Expr callee; std::vector args; }; +struct Assign { Name lhs; Expr rhs; }; +struct If; +struct While; +struct Block; +using Stmt = rvariant, recursive_wrapper, recursive_wrapper>; +struct Block { std::vector stmts; }; +struct If { Expr cond; Block then; std::optional otherwise; }; +struct While { Expr cond; Block body; }; +struct Lambda { std::vector params; Block body; }; + +} // ast + +template +constexpr write_rank rank = x4::write_rank_v; + +// `value` written as an rvalue and as a `const&` into copies of `initial` +template +void check_write(S const& initial, V const& value, S const& expected) +{ + { + S s = initial; + V v = value; + x4::write_attribute(s, std::move(v)); + CHECK(s == expected); + } + { + S s = initial; + x4::write_attribute(s, value); + CHECK(s == expected); + } +} + +IRIS_ALLOY_ADAPT_STRUCT(Span, first, last); +IRIS_ALLOY_ADAPT_STRUCT(Paren, inner); +IRIS_ALLOY_ADAPT_STRUCT(Ident, name); +IRIS_ALLOY_ADAPT_STRUCT(Assign, lhs, rhs); +IRIS_ALLOY_ADAPT_STRUCT(Compare, lhs, rhs); +IRIS_ALLOY_ADAPT_STRUCT(Literal, value); +IRIS_ALLOY_ADAPT_STRUCT(Real, value); +IRIS_ALLOY_ADAPT_STRUCT(Count, value); +IRIS_ALLOY_ADAPT_STRUCT(Ints, values); +IRIS_ALLOY_ADAPT_STRUCT(ReturnStmt, value); +IRIS_ALLOY_ADAPT_STRUCT(ExprStmt, value); +IRIS_ALLOY_ADAPT_STRUCT(Number, value); +IRIS_ALLOY_ADAPT_STRUCT(Point, x, y); +IRIS_ALLOY_ADAPT_STRUCT(LongLongNumber, value); +IRIS_ALLOY_ADAPT_STRUCT(VariantNumber, value); +IRIS_ALLOY_ADAPT_STRUCT(binop, l, op, r); +IRIS_ALLOY_ADAPT_STRUCT(Tree, v, kids); +IRIS_ALLOY_ADAPT_STRUCT(Items, items); +IRIS_ALLOY_ADAPT_STRUCT(Box, inner); +IRIS_ALLOY_ADAPT_STRUCT(Single, n); +IRIS_ALLOY_ADAPT_STRUCT(BoxOrSingle, inner); +IRIS_ALLOY_ADAPT_STRUCT(BoxOrOptional, inner); +IRIS_ALLOY_ADAPT_STRUCT(SingleA, n); +IRIS_ALLOY_ADAPT_STRUCT(SingleB, n); +IRIS_ALLOY_ADAPT_STRUCT(CycleBoxA, inner); +IRIS_ALLOY_ADAPT_STRUCT(CycleBoxB, inner); +IRIS_ALLOY_ADAPT_STRUCT(HoldsCycleA, a); +IRIS_ALLOY_ADAPT_STRUCT(AgreeBoxA, inner); +IRIS_ALLOY_ADAPT_STRUCT(AgreeBoxB, inner); +IRIS_ALLOY_ADAPT_STRUCT(TieBoxM, inner); +IRIS_ALLOY_ADAPT_STRUCT(TieBoxN, inner); +IRIS_ALLOY_ADAPT_STRUCT(Xs, xs); +IRIS_ALLOY_ADAPT_STRUCT(ast::Name, text); +IRIS_ALLOY_ADAPT_STRUCT(ast::Binary, lhs, op, rhs); +IRIS_ALLOY_ADAPT_STRUCT(ast::Unary, op, operand); +IRIS_ALLOY_ADAPT_STRUCT(ast::Call, callee, args); +IRIS_ALLOY_ADAPT_STRUCT(ast::Assign, lhs, rhs); +IRIS_ALLOY_ADAPT_STRUCT(ast::Block, stmts); +IRIS_ALLOY_ADAPT_STRUCT(ast::If, cond, then, otherwise); +IRIS_ALLOY_ADAPT_STRUCT(ast::While, cond, body); +IRIS_ALLOY_ADAPT_STRUCT(ast::Lambda, params, body); + +TEST_CASE("write_rank") +{ + constexpr auto none = write_rank::none; + constexpr auto converts = write_rank::assignable_without_narrowing; + constexpr auto structural = write_rank::structural; + constexpr auto exact = write_rank::exact; + + // plain: the assignment of the language without narrowing + STATIC_CHECK(rank == exact); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == none); + STATIC_CHECK(rank == converts); + // narrowed into the parameter of a user-defined conversion or assignment operator: the responsibility of the type + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == none); + STATIC_CHECK(rank == none); // would dangle + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank, std::unique_ptr> == exact); + STATIC_CHECK(rank, std::unique_ptr const&> == none); + STATIC_CHECK(rank == exact); // not a range of itself + + // container: appending a range + STATIC_CHECK(rank, std::vector> == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank, std::vector> == converts); + STATIC_CHECK(rank, int> == none); + STATIC_CHECK(rank == none); + STATIC_CHECK(rank, std::vector> == structural); + STATIC_CHECK(rank, std::vector>> == structural); + STATIC_CHECK(rank>, std::string> == structural); + STATIC_CHECK(rank>>, std::vector> == structural); + STATIC_CHECK(rank, std::vector> == converts); // through the proxy reference + STATIC_CHECK(rank, std::vector>> == structural); + STATIC_CHECK(rank, std::vector> == structural); + STATIC_CHECK(rank, std::optional> == none); + STATIC_CHECK(rank, std::vector> == none); + + // tuple-like: member by member; a single-element tuple-like holds its element + STATIC_CHECK(rank> == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank> == none); + STATIC_CHECK(rank> == none); + STATIC_CHECK(rank == structural); // the alternative of its own type + STATIC_CHECK(rank == none); + + // optional + STATIC_CHECK(rank, int> == structural); + STATIC_CHECK(rank, NoDefault> == structural); + STATIC_CHECK(rank, Span> == structural); + STATIC_CHECK(rank>, std::vector> == structural); + STATIC_CHECK(rank>, int> == none); + STATIC_CHECK(rank, int> == none); + STATIC_CHECK(rank, int> == none); // copy-initializable, but direct-initialization is ambiguous + STATIC_CHECK(rank, std::optional> == converts); + STATIC_CHECK(rank, long long> == none); + STATIC_CHECK(rank> == structural); + + // rvariant: the same type, a conversion, by the shape, then wrapped + STATIC_CHECK(rank>, std::vector> == structural); + STATIC_CHECK(rank, int> == converts); + STATIC_CHECK(rank, int> == none); + STATIC_CHECK(rank, long long>, int> == converts); + STATIC_CHECK(rank, char> == converts); + STATIC_CHECK(rank, long long>, int> == converts); + STATIC_CHECK(rank, alloy::tuple> == none); + STATIC_CHECK(rank, NoDefault> == structural); + STATIC_CHECK(rank, alloy::tuple> == none); + STATIC_CHECK(rank, int> == structural); + STATIC_CHECK(rank, int> == none); // ambiguous +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4244) +#endif + STATIC_CHECK(rank, long long> == converts); // Port +#ifdef _MSC_VER +# pragma warning(pop) +#endif + STATIC_CHECK(rank, int> == converts); // long long, by a standard conversion + STATIC_CHECK(rank, int> == none); // ambiguous, both by a user-defined conversion + + // rvariant from an rvariant: into the same type, alternative by alternative, then wrapped + STATIC_CHECK(rank>, rvariant> == structural); + STATIC_CHECK(rank, rvariant> == converts); + STATIC_CHECK(rank, rvariant> == none); + STATIC_CHECK(rank, rvariant> == none); // each state ambiguous + + // wrapped only when the value becomes the element as it is or by a standard conversion + STATIC_CHECK(rank, int> == converts); + STATIC_CHECK(rank, int> == converts); + STATIC_CHECK(rank, long long> == structural); + STATIC_CHECK(rank, long long> == converts); // Port, as a conversion precedes wrapping + STATIC_CHECK(rank, double> == none); + STATIC_CHECK(rank, std::vector> == structural); + STATIC_CHECK(rank, int> == none); + STATIC_CHECK(rank, Expr> == none); + STATIC_CHECK(rank, rvariant> == none); + STATIC_CHECK(rank, std::vector> == structural); + + // recursive types + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank, std::vector> == structural); // flattened + STATIC_CHECK(rank> == none); + STATIC_CHECK(rank> == structural); + STATIC_CHECK(rank == none); // an optional in itself without an end + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == none); // a conflict + STATIC_CHECK(rank == none); + STATIC_CHECK(rank == none); // into the conflict + STATIC_CHECK(rank, int> == none); + STATIC_CHECK(rank, std::vector> == none); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == converts); + STATIC_CHECK(rank == none); // ambiguous + STATIC_CHECK(rank == none); // into tie_m + STATIC_CHECK(rank> == structural); + STATIC_CHECK(rank> == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == structural); + STATIC_CHECK(rank == converts); + + // not an attribute + STATIC_CHECK(rank == none); + STATIC_CHECK(rank == none); +} + +TEST_CASE("write_attribute") +{ + // plain + check_write(0LL, 42, 42LL); + check_write(0, 'c', 99); + check_write(Port{0}, 3, Port{3}); +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4244) +#endif + check_write(Port{0}, 5LL, Port{5}); + check_write(Wrap{0}, 7LL, Wrap{7}); +#ifdef _MSC_VER +# pragma warning(pop) +#endif + + // container: appended, never replaced + check_write(std::vector{1, 2}, std::vector{3}, std::vector{1, 2, 3}); + check_write(std::string("x"), std::string("ab"), std::string("xab")); + check_write(std::set{1, 5}, std::vector{3, 5}, std::set{1, 3, 5}); + check_write(std::vector{"x"}, std::vector{'a', 'b'}, std::vector{"x", "ab"}); + check_write(std::vector{}, std::vector>{{'a', 'b'}, {'c'}}, std::vector{"ab", "c"}); + check_write(std::vector>{}, std::string("abc"), std::vector>{std::string("abc")}); + check_write( + std::vector>>{}, + std::vector{1, 2}, + std::vector>>{std::vector{1, 2}} + ); + check_write( + std::vector{}, + std::vector>{{'a', '1'}, {'b', '2'}}, + std::vector{"a1b2"} + ); + check_write(std::vector{}, std::vector>{{1, 2}, {3, 4}}, std::vector{1, 2, 3, 4}); + check_write(std::vector{}, std::vector>{'a', std::nullopt, 'b'}, std::vector{'a', 'b'}); + check_write(std::vector{}, std::vector{Span(1, 4)}, std::vector{Span(1, 4)}); + + // tuple-like + check_write(Point{}, alloy::tuple{1, 2}, Point{1, 2}); + check_write(Paren{}, 1, Paren{Expr{1}}); + + // optional: into the content if any, else a new one + check_write(std::optional{}, 1, std::optional{1}); + check_write(std::optional{5}, 1, std::optional{1}); + check_write(std::optional{}, NoDefault{3}, std::optional{NoDefault{3}}); + check_write(std::optional{}, Span(1, 4), std::optional{Span(1, 4)}); + check_write(std::optional>{{1, 2}}, std::vector{3}, std::optional>{{1, 2, 3}}); + check_write(std::optional{5}, std::optional{}, std::optional{}); + check_write(std::optional{}, std::optional{7}, std::optional{7}); + check_write('x', std::optional{}, 'x'); + check_write('x', std::optional{'c'}, 'c'); + + // rvariant + using string_or_chars = rvariant>; + check_write(string_or_chars{}, std::vector{'a'}, string_or_chars{std::vector{'a'}}); + check_write(rvariant{}, 42, rvariant{42LL}); + check_write(rvariant, long long>{}, 42, rvariant, long long>{42LL}); + check_write(rvariant{}, 'c', rvariant{99}); + check_write(rvariant, long long>{}, 42, rvariant, long long>{42LL}); + check_write(rvariant{}, NoDefault{3}, rvariant{NoDefault{3}}); + check_write(rvariant{}, 3, rvariant{std::in_place_index<0>, 3}); +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4244) +#endif + check_write(rvariant{0}, 5LL, rvariant{Port{5}}); + check_write(rvariant{0LL}, 5, rvariant{5LL}); + check_write(rvariant{Literal{}}, 5LL, rvariant{Port{5}}); +#ifdef _MSC_VER +# pragma warning(pop) +#endif + + using nested = rvariant>; + check_write(nested{}, rvariant{'a'}, nested{rvariant{'a'}}); + check_write(rvariant{}, rvariant{'b'}, rvariant{98LL}); + check_write(rvariant{}, rvariant{1}, rvariant{1LL}); + + check_write(rvariant{}, 5, rvariant{Literal{5}}); + check_write(rvariant{}, 5, rvariant{5LL}); + check_write(rvariant{}, std::vector{1, 2}, rvariant{Ints{{1, 2}}}); + { + rvariant s; + x4::write_attribute(s, 7LL); + CHECK(iris::get(s).value == 7); + } + { + rvariant s; + x4::write_attribute(s, std::vector(2)); + CHECK(iris::get(s).xs.size() == 2); + } + + // the alternative held is written into + using ints_or_int = rvariant>; + check_write(ints_or_int{std::vector{1}}, std::vector{2}, ints_or_int{std::vector{1, 2}}); + check_write(rvariant{Ints{{1}}}, std::vector{2}, rvariant{Ints{{1, 2}}}); + + // recursive types + { + binop const value{1, '+', recursive_wrapper(binop{2, '*', 3})}; + check_write(binop{}, value, value); + } + { + std::vector const tree{Tree{1, {Tree{2, {}}, Tree{3, {Tree{4, {}}}}}}, Tree{5, {}}}; + check_write(std::vector{}, tree, std::vector{1, 2, 3, 4, 5}); + } + { + box_or_single s; + x4::write_attribute(s, 1); + CHECK(iris::get(s).n == 1); + } + { + box_or_optional s; + x4::write_attribute(s, 1); + CHECK(iris::get>(s) == 1); + } + { + cycle_a s; + x4::write_attribute(s, CycleBoxB{cycle_b{SingleB{1}}}); + CHECK(iris::get((*iris::get<0>(s))->inner).n == 1); + } + { + agree_a s; + x4::write_attribute(s, 1); + CHECK(iris::get((*iris::get<0>(s))->inner) == 1); + } + check_write(IntArray{}, std::vector{1, 2}, IntArray{int_node{1}, int_node{2}}); + check_write(int_node{}, std::vector{1, 2}, int_node{IntArray{int_node{1}, int_node{2}}}); + check_write(StringArray{}, std::string("ab"), StringArray{string_node{std::string("ab")}}); + { + ast::Stmt s; + x4::write_attribute(s, ast::Stmt{recursive_wrapper(ast::While{ast::Expr{1LL}, ast::Block{{ast::Stmt{ast::Expr{2.5}}}}})}); + auto const& loop = iris::get(s); + CHECK(iris::get(loop.cond) == 1); + CHECK(iris::get(iris::get(loop.body.stmts.at(0))) == 2.5); + } +} + +template +struct PushOnly +{ + using value_type = T; + std::vector data; + + [[nodiscard]] auto begin() noexcept { return data.begin(); } + [[nodiscard]] auto end() noexcept { return data.end(); } + void clear() noexcept { data.clear(); } + void push_back(T value) { data.emplace_back(std::move(value)); } +}; + +// - Can be default constructed +// - Can be constructed from `int` +// - Can assign `int` +struct WeakNumber +{ + WeakNumber() = default; + WeakNumber(int) : constructed_from_int_and_never_reassigned(true) {} + WeakNumber& operator=(int) { constructed_from_int_and_never_reassigned = false; return *this; } + bool constructed_from_int_and_never_reassigned = false; +}; + +// - Can be default constructed +// - Can NOT be converted from `int` as constructor being `explicit` +// - Can assign `int` +struct StrongNumber +{ + StrongNumber() = default; + explicit StrongNumber(int) {} + StrongNumber& operator=(int) { return *this; } +}; + +struct ID_Param +{ + WeakNumber id; + std::optional param; +}; +IRIS_ALLOY_ADAPT_STRUCT(ID_Param, id, param); + +TEST_CASE("new object") +{ + using x4::int_; + using x4::is_writable_v; + + // A new plain object is constructed from the value, by `write_attribute` and while parsing alike + { + std::vector v; + x4::write_attribute(v, std::vector{1}); + CHECK(v[0].constructed_from_int_and_never_reassigned); + } + { + std::vector v; + REQUIRE(parse("1", int_ % ',', v)); + CHECK(v[0].constructed_from_int_and_never_reassigned); + } + { + std::optional o; + x4::write_attribute(o, 1); + REQUIRE(o.has_value()); + CHECK(o->constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + { + std::optional o; + REQUIRE(parse("1", -int_, o)); + REQUIRE(o.has_value()); + CHECK(o->constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + { + std::optional o; + REQUIRE(parse("1", int_, o)); + REQUIRE(o.has_value()); + CHECK(o->constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + { + rvariant var; + x4::write_attribute(var, 1); + CHECK(iris::get<1>(var).constructed_from_int_and_never_reassigned); + } + { + rvariant var; + REQUIRE(parse("1", int_, var)); + CHECK(iris::get<1>(var).constructed_from_int_and_never_reassigned); + } + + { + std::optional> o; + x4::write_attribute(o, 1); + REQUIRE(o.has_value()); + CHECK(iris::get<0>(*o).constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + { + std::vector> v; + REQUIRE(parse("1", int_ % ',', v)); + CHECK(iris::get<0>(v[0]).constructed_from_int_and_never_reassigned); + } + { + rvariant>> var; + x4::write_attribute(var, 1); + CHECK(iris::get<0>(iris::get<1>(var)).constructed_from_int_and_never_reassigned); + } + { + std::vector> v; + x4::write_attribute(v, std::vector>{1}); + CHECK(iris::get<0>(v[0]).constructed_from_int_and_never_reassigned); + } + // An element of `std::set` cannot be written in place; it is made first and then inserted + { + std::set> s; + x4::write_attribute(s, std::vector{2, 1}); + CHECK(s == std::set>{1, 2}); + } + { + std::set> s; + x4::write_attribute(s, std::vector>{{1, 2}}); + CHECK(s == std::set>{{1, 2}}); + } + { + using Value = rvariant; + STATIC_CHECK(x4::traits::X4Container>); + STATIC_CHECK(x4::is_writable_v&, std::vector>); + PushOnly c; + x4::write_attribute(c, std::vector{1, 2}); + CHECK(c.data == std::vector{1, 2}); + + PushOnly> numbers; + x4::write_attribute(numbers, std::vector{1}); + REQUIRE(numbers.data.size() == 1); + CHECK(iris::get<0>(numbers.data[0]).constructed_from_int_and_never_reassigned); + } + + { + std::optional o{std::in_place}; + x4::write_attribute(o, 1); + CHECK(!o->constructed_from_int_and_never_reassigned); + } + { + rvariant var{std::in_place_index<1>}; + x4::write_attribute(var, 1); + CHECK(!iris::get<1>(var).constructed_from_int_and_never_reassigned); + } + { + // The default state holds `WeakNumber`, as `std::variant v; v = 1;` assigns + rvariant var; + REQUIRE(parse("1", int_, var)); + CHECK(!iris::get<0>(var).constructed_from_int_and_never_reassigned); + } + { + ID_Param idp; + x4::write_attribute(idp, alloy::tuple{1, 2}); + CHECK(!idp.id.constructed_from_int_and_never_reassigned); + REQUIRE(idp.param.has_value()); + CHECK(idp.param->constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + { + ID_Param idp; + REQUIRE(parse("1,2", int_ >> ',' >> int_, idp)); + CHECK(!idp.id.constructed_from_int_and_never_reassigned); + REQUIRE(idp.param.has_value()); + CHECK(idp.param->constructed_from_int_and_never_reassigned); // NOLINT(bugprone-unchecked-optional-access) + } + + // No default constructor is needed + check_write(std::vector{}, std::vector{1, 2}, std::vector{Port{1}, Port{2}}); + { + std::vector v; + REQUIRE(parse("1,2", int_ % ',', v)); + CHECK(v == std::vector{Port{1}, Port{2}}); + } + { + constexpr auto ports = x4::rule>{"ports"} = *(int_ >> ';'); + std::vector v; + REQUIRE(parse("1;2;", ports, v)); + CHECK(v == std::vector{Port{1}, Port{2}}); + } + { + // The failed parse adds nothing + std::vector v; + auto const result = parse("1;2", *(int_ >> ';'), v); + REQUIRE(result.is_partial_match()); + CHECK(result.remainder_str() == "2"); + CHECK(v == std::vector{Port{1}}); + } + { + std::optional o; + auto const result = parse("1", -(int_ >> ';'), o); + REQUIRE(result.is_partial_match()); + CHECK(result.remainder_str() == "1"); + CHECK(!o); + } + + // A new plain object is never default-constructed-then-assigned. + // See also: `creatable_in_place_for`. + STATIC_CHECK(is_writable_v); + STATIC_CHECK(!is_writable_v&, int>); + STATIC_CHECK(!is_writable_v&, std::vector>); + STATIC_CHECK(!is_writable_v&, int>); + STATIC_CHECK(x4::detail::container_parse_strategy_for, std::vector> == x4::detail::container_parse_strategy::none); +} + +TEST_CASE("is_writable") +{ + using x4::is_writable_v; + + // Identical types + STATIC_CHECK(is_writable_v); + STATIC_CHECK(is_writable_v&, std::vector>); + STATIC_CHECK(is_writable_v&, alloy::tuple>); + STATIC_CHECK(is_writable_v&, iris::rvariant>); + + // `iris::rvariant` is "broader" than `int` + STATIC_CHECK( is_writable_v&, int>); + STATIC_CHECK(!is_writable_v>); + + // Container types + STATIC_CHECK(is_writable_v>&, std::vector>); + + // Tuple-like types + STATIC_CHECK(is_writable_v>&, alloy::tuple>); +} diff --git a/test/x4/x3_rule_problem.cpp b/test/x4/x3_rule_problem.cpp index 6a6a8e7c2..86e31961f 100644 --- a/test/x4/x3_rule_problem.cpp +++ b/test/x4/x3_rule_problem.cpp @@ -9,59 +9,137 @@ #include "iris_x4_test.hpp" #include +#include +#include +#include +#include +#include +#include + +#include +#include +#include -#include #include +#include +#include +#include +#include + +namespace { + +enum class strong_int : int {}; + +struct converted_from_int +{ + converted_from_int(int) {} // NOLINT(misc-explicit-constructor) +}; + +struct assigned_from_int +{ + explicit assigned_from_int(int) {} + assigned_from_int& operator=(int) { return *this; } +}; + +struct Paren +{ + int inner = 0; + bool operator==(Paren const&) const = default; +}; + +struct Box +{ + double value = 0; +}; +} // anonymous + +IRIS_ALLOY_ADAPT_STRUCT(Paren, inner); +IRIS_ALLOY_ADAPT_STRUCT(Box, value); + +constexpr x4::rule int_rule = "int_rule"; +constexpr x4::rule> ints_rule = "ints_rule"; +constexpr x4::rule double_rule = "double_rule"; +constexpr x4::rule paren_rule = "paren_rule"; + +constexpr auto int_rule_def = int_rule = x4::int_; +constexpr auto ints_rule_def = ints_rule = x4::int_ % ','; +constexpr auto double_rule_def = double_rule = x4::double_; +constexpr auto paren_rule_def = paren_rule = '(' >> int_rule >> ')'; + +IRIS_X4_DEFINE(int_rule) +IRIS_X4_DEFINE(ints_rule) +IRIS_X4_DEFINE(double_rule) +IRIS_X4_DEFINE(paren_rule) + +// "The Spirit X3 rule problem" in Boost.Parser's documentation +// https://www.boost.org/doc/libs/1_89_0/doc/html/boost_parser/this_library_s_relationship_to_boost_spirit.html#boost_parser.this_library_s_relationship_to_boost_spirit.the_spirit_x3_rule_problem +// https://github.com/boostorg/spirit_x4/issues/38 TEST_CASE("x3_rule_problem") { - enum class strong_int : int {}; + using x4::X4StrictlyWritable; - // Primitive (int) - { - using It = std::string_view::const_iterator; - using Se = It; - using Rule = x4::rule; + STATIC_CHECK(X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); // assignable, but not implicitly convertible + STATIC_CHECK(X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); - STATIC_CHECK(x4::is_parsable_v, int>); - STATIC_CHECK(x4::is_parsable_v, long long>); + STATIC_CHECK(X4StrictlyWritable); - // Narrowing conversion - STATIC_CHECK(!x4::is_parsable_v, short>); - STATIC_CHECK(!x4::is_parsable_v, unsigned long long>); - STATIC_CHECK(!x4::is_parsable_v, double>); + STATIC_CHECK(!X4StrictlyWritable&, std::vector&&>); + STATIC_CHECK(!X4StrictlyWritable&, std::vector&&>); + STATIC_CHECK(!X4StrictlyWritable); + STATIC_CHECK(!X4StrictlyWritable); - // Not permitted as of now, but can be relaxed in the future - STATIC_CHECK(!x4::detail::RuleAttrTransformable); - STATIC_CHECK(!x4::is_parsable_v, strong_int>); + STATIC_CHECK(X4StrictlyWritable); + STATIC_CHECK(x4::detail::dangles); + + { + long long n = 0; + REQUIRE(parse("42", int_rule, n)); + CHECK(n == 42); + } + { + std::vector v; + REQUIRE(parse("1,2", ints_rule, v)); + CHECK(v == std::vector{1, 2}); } - // Primitive (double) { - using It = std::string_view::const_iterator; - using Se = It; - using Rule = x4::rule; - - STATIC_CHECK(x4::is_parsable_v, double>); - STATIC_CHECK(x4::is_parsable_v, long double>); - - // Narrowing conversion - STATIC_CHECK(!x4::is_parsable_v, int>); - STATIC_CHECK(!x4::is_parsable_v, long long>); - STATIC_CHECK(!x4::is_parsable_v, unsigned long long>); - STATIC_CHECK(!x4::is_parsable_v, float>); + Paren p; + REQUIRE(parse("7", int_rule, p)); + CHECK(p == Paren{7}); + REQUIRE(parse("(1)", paren_rule, p)); + CHECK(p == Paren{1}); } - // "The Spirit X3 rule problem" in Boost.Parser's documentation - // https://www.boost.org/doc/libs/1_89_0/doc/html/boost_parser/this_library_s_relationship_to_boost_spirit.html#boost_parser.this_library_s_relationship_to_boost_spirit.the_spirit_x3_rule_problem - // https://github.com/boostorg/spirit_x4/issues/38 { - using It = std::string_view::const_iterator; - using Se = It; - using Rule = x4::rule>; + using variant = iris::rvariant, Box>; + constexpr std::string_view input = "3.5"; + + variant assigned; + auto first = input.begin(); +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4244) +#endif + REQUIRE(double_rule.parse(first, input.end(), x4::unused, assigned)); +#ifdef _MSC_VER +# pragma warning(pop) +#endif + CHECK(std::get<0>(iris::get<0>(assigned)) == 3); + + variant written; + x4::write_attribute(written, 3.5); + CHECK(iris::get<1>(written).value == 3.5); - STATIC_CHECK(x4::is_parsable_v, std::vector>); - STATIC_CHECK(!x4::is_parsable_v, std::set>); - STATIC_CHECK(!x4::is_parsable_v, std::vector>); + variant prepared; + REQUIRE(parse(input, double_rule, prepared)); + CHECK(iris::get<1>(prepared).value == 3.5); } }