From 24f37505582abdde9f2d53871378190b81543dea Mon Sep 17 00:00:00 2001 From: sentomk Date: Sat, 1 Aug 2026 17:06:58 +0800 Subject: [PATCH] feat(pattern): add operator sugar for combinators Problem - The pattern algebra had only function-call forms (neg, any, all), which read noisily in case lists compared to the boolean operators they model. Implementation - Add operator! for neg(p), operator|| for any(a, b), and operator&& for all(a, b). - Declare the overloads in ptn::pat::base: every pattern derives from pattern_base, making that namespace ADL-associated for all pattern types (concrete patterns live in ptn::pat::detail and ADL does not ascend namespaces). - Constrain operands to patterns that are not guard predicates, so guard-level && / || keep their pred_and / pred_or semantics. Tests - Nine cases in tests_combinator: type identity of !p vs neg(p), or/and/chained/double-bang behavior, sugar on type patterns, and guard-operator non-interference. - Full suite: 204/204 (Clang 20, C++17). Notes - `>>` binds tighter than || and &&, so combined patterns need parentheses in a case: (lit(1) || lit(2)) >> handler. Unary ! needs none. Documented in docs/api.md and README. --- README.md | 6 ++- docs/api.md | 10 ++++ docs/roadmap.md | 10 ++++ include/ptn/pattern/combinator.hpp | 65 ++++++++++++++++++++--- include/ptn/pattern/negation.hpp | 20 +++++++ tests/tests_combinator.cpp | 85 ++++++++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fed49362..bad2544d 100644 --- a/README.md +++ b/README.md @@ -137,10 +137,12 @@ std::string describe(const Value &v) { // Negation: match values NOT equal to specific literals int status = 404; auto msg = match(status) | on( - neg(val<200>) >> []{ return std::string("error"); }, - _ >> []{ return std::string("ok"); } + !val<200> >> []{ return std::string("error"); }, + _ >> []{ return std::string("ok"); } ); // msg == "error" — status isn't 200 +// `!p` is sugar for neg(p); likewise (a || b) for any and +// (a && b) for all. ``` ## Installation diff --git a/docs/api.md b/docs/api.md index e5f573a8..aa7c98c0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -344,6 +344,8 @@ Properties: - Sub-patterns are evaluated left-to-right; evaluation stops at the first match. - Requires at least one sub-pattern; every argument must be a pattern object. +- Operator sugar: `(a || b)` is equivalent to `any(a, b)`. Note that `>>` + binds tighter than `||`, so parenthesize: `(lit(1) || lit(2)) >> handler`. ### `all(ps...)` @@ -363,6 +365,8 @@ Properties: - Sub-patterns are evaluated left-to-right; evaluation stops at the first mismatch. - Requires at least one sub-pattern; every argument must be a pattern object. +- Operator sugar: `(a && b)` is equivalent to `all(a, b)`. Note that `>>` + binds tighter than `&&`, so parenthesize: `(p && q) >> handler`. ### `neg(p)` @@ -381,6 +385,12 @@ Properties: - Non-binding: handlers receive zero arguments. - Accepts exactly one sub-pattern (no zero- or multi-argument form). - `neg(neg(p))` restores the original match behavior (double negation cancels). +- Operator sugar: `!p` is equivalent to `neg(p)` and needs no parentheses: + `!val<200> >> "error"`. + +The pattern-level operators only accept pattern operands, so they never +collide with the guard-level `&&` / `||` (which keep their `pred_and` / +`pred_or` meaning inside `[...]` guards). --- diff --git a/docs/roadmap.md b/docs/roadmap.md index 75ca071a..f8a35300 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,6 +71,16 @@ without static reflection. --- +### Pattern operator sugar — `!p`, `(a || b)`, `(a && b)` + +Operator forms of the combinators: `!p` for `neg(p)`, `(a || b)` for +`any(a, b)`, and `(a && b)` for `all(a, b)`. The overloads live in +`ptn::pat::base` so ADL finds them for every pattern via the shared +`pattern_base` base class, and they reject guard predicates so the +guard-level `&&` / `||` semantics are untouched. + +--- + ## NEXT Potential follow-up items after current WIP scope is stabilized. diff --git a/include/ptn/pattern/combinator.hpp b/include/ptn/pattern/combinator.hpp index 17fa2708..85c5d408 100644 --- a/include/ptn/pattern/combinator.hpp +++ b/include/ptn/pattern/combinator.hpp @@ -12,6 +12,7 @@ #include "ptn/pattern/base/fwd.h" #include "ptn/pattern/base/pattern_base.hpp" +#include "ptn/pattern/base/pattern_traits.hpp" namespace ptn::pat { @@ -20,11 +21,13 @@ namespace ptn::pat { // Matches when any sub-pattern matches. This combinator never // contributes bindings; it is used only for control flow. template - struct any_pattern : base::pattern_base> { + struct any_pattern + : base::pattern_base> { std::tuple patterns; template > + typename = std::enable_if_t> constexpr explicit any_pattern(Ps &&...ps) : patterns(std::forward(ps)...) { } @@ -49,11 +52,13 @@ namespace ptn::pat { // Matches only when every sub-pattern matches. Like any_pattern, // this combinator is non-binding. template - struct all_pattern : base::pattern_base> { + struct all_pattern + : base::pattern_base> { std::tuple patterns; template > + typename = std::enable_if_t> constexpr explicit all_pattern(Ps &&...ps) : patterns(std::forward(ps)...) { } @@ -85,7 +90,8 @@ namespace ptn::pat { sizeof...(Patterns) > 0, "[Patternia.any]: requires at least one sub-pattern."); static_assert( - (std::is_base_of_v> && ...), + (std::is_base_of_v> + && ...), "[Patternia.any]: every argument must be a pattern object."); return detail::any_pattern...>( @@ -100,7 +106,8 @@ namespace ptn::pat { sizeof...(Patterns) > 0, "[Patternia.all]: requires at least one sub-pattern."); static_assert( - (std::is_base_of_v> && ...), + (std::is_base_of_v> + && ...), "[Patternia.all]: every argument must be a pattern object."); return detail::all_pattern...>( @@ -111,13 +118,55 @@ namespace ptn::pat { namespace ptn::pat::base { + namespace detail { + + // Both operands must be patterns and neither may be a guard + // predicate: `&&` / `||` between guard predicates keep their + // existing pred_and / pred_or meaning. + template + inline constexpr bool pattern_pair_v = + std::is_base_of_v> + && std::is_base_of_v> + && !pat::traits::is_guard_predicate_v> + && !pat::traits::is_guard_predicate_v>; + + } // namespace detail + + // Operator sugar: `a || b` is equivalent to `any(a, b)`. + // + // Declared in ptn::pat::base so ADL finds it for every pattern + // via the shared pattern_base base class (concrete patterns live + // in ptn::pat::detail, and ADL does not ascend namespaces). + // + // NOTE: `>>` binds tighter than `||`, so parenthesize the + // pattern in a case: `(lit(1) || lit(2)) >> handler`. + template , int> = 0> + constexpr auto operator||(L &&l, R &&r) { + return pat::any(std::forward(l), std::forward(r)); + } + + // Operator sugar: `a && b` is equivalent to `all(a, b)`. + // + // NOTE: `>>` binds tighter than `&&`, so parenthesize the + // pattern in a case: `(p && q) >> handler`. + template , int> = 0> + constexpr auto operator&&(L &&l, R &&r) { + return pat::all(std::forward(l), std::forward(r)); + } + template - struct binding_args, Subject> { + struct binding_args, + Subject> { using type = std::tuple<>; }; template - struct binding_args, Subject> { + struct binding_args, + Subject> { using type = std::tuple<>; }; diff --git a/include/ptn/pattern/negation.hpp b/include/ptn/pattern/negation.hpp index fbc80e52..41098ae4 100644 --- a/include/ptn/pattern/negation.hpp +++ b/include/ptn/pattern/negation.hpp @@ -5,6 +5,8 @@ #include #include "ptn/pattern/base/fwd.h" +#include "ptn/pattern/base/pattern_base.hpp" +#include "ptn/pattern/base/pattern_traits.hpp" namespace ptn::pat { @@ -46,4 +48,22 @@ namespace ptn::pat::base { using type = std::tuple<>; }; + // Operator sugar: `!p` is equivalent to `neg(p)`. Only pattern + // objects participate; guard predicates are excluded so this + // never collides with boolean logic over predicates. + // + // Declared in ptn::pat::base so ADL finds it for every pattern: + // all patterns derive from base::pattern_base, which makes this + // namespace associated even though the concrete pattern types + // live in ptn::pat::detail (ADL does not ascend namespaces). + template < + typename P, + std::enable_if_t< + std::is_base_of_v> + && !pat::traits::is_guard_predicate_v>, + int> = 0> + constexpr auto operator!(P &&p) { + return pat::neg(std::forward

(p)); + } + } // namespace ptn::pat::base diff --git a/tests/tests_combinator.cpp b/tests/tests_combinator.cpp index 08a530c5..4c472a5e 100644 --- a/tests/tests_combinator.cpp +++ b/tests/tests_combinator.cpp @@ -336,3 +336,88 @@ TEST(NegationPattern, NegNegIsIdentity) { _ >> [] { return 0; }), 42); } + +// ========================================================================= +// Operator sugar: !p == neg(p), (a || b) == any(a, b), +// (a && b) == all(a, b). +// ========================================================================= + +TEST(OperatorSugar, NegationIsNeg) { + static_assert( + std::is_same_v); +} + +TEST(OperatorSugar, BangMatchesNeg) { + int a = 5, b = 1; + EXPECT_EQ(match(a) | on(!lit(1) >> 1, _ >> 0), 1); + EXPECT_EQ(match(b) | on(!lit(1) >> 1, _ >> 0), 0); +} + +TEST(OperatorSugar, BangOnVal) { + int a = 404, b = 200; + EXPECT_EQ(match(a) | on(!val<200> >> 1, _ >> 0), 1); + EXPECT_EQ(match(b) | on(!val<200> >> 1, _ >> 0), 0); +} + +TEST(OperatorSugar, OrIsAny) { + int a = 2, b = 3; + // NOTE: `>>` binds tighter than `||`, hence the parentheses. + EXPECT_EQ(match(a) | on((lit(1) || lit(2)) >> 1, _ >> 0), 1); + EXPECT_EQ(match(b) | on((lit(1) || lit(2)) >> 1, _ >> 0), 0); +} + +TEST(OperatorSugar, OrChainsLeft) { + int a = 3, b = 4; + EXPECT_EQ(match(a) | on((lit(1) || lit(2) || lit(3)) >> 1, _ >> 0), + 1); + EXPECT_EQ(match(b) | on((lit(1) || lit(2) || lit(3)) >> 1, _ >> 0), + 0); +} + +TEST(OperatorSugar, AndIsAll) { + auto is_even = [](int x) { return x % 2 == 0; }; + int a = 6, b = 2, c = 5; + EXPECT_EQ(match(a) | on((pred(is_even) && !lit(2)) >> 1, _ >> 0), + 1); + EXPECT_EQ(match(b) | on((pred(is_even) && !lit(2)) >> 1, _ >> 0), + 0); + EXPECT_EQ(match(c) | on((pred(is_even) && !lit(2)) >> 1, _ >> 0), + 0); +} + +TEST(OperatorSugar, DoubleBangIsIdentity) { + int a = 1, b = 2; + EXPECT_EQ(match(a) | on(!!lit(1) >> 1, _ >> 0), 1); + EXPECT_EQ(match(b) | on(!!lit(1) >> 1, _ >> 0), 0); +} + +TEST(OperatorSugar, GuardOperatorsUnaffected) { + // `&&` / `||` between guard predicates keep pred_and / pred_or + // semantics; the pattern-level sugar must not interfere. + int a = 50, b = 150; + EXPECT_EQ(match(a) | on($[(_ > 0) && (_ < 100)] >> 1, _ >> 0), 1); + EXPECT_EQ(match(b) | on($[(_ < 0) || (_ > 100)] >> 1, _ >> 0), 1); +} + +TEST(OperatorSugar, MixedWithTypePatterns) { + // The sugar composes patterns of different kinds against one + // subject (here: two type patterns over a variant). + std::variant a = 1; + std::variant b = std::string("s"); + std::variant c = 1.5; + EXPECT_EQ( + match(a) + | on((is || is) >> [] { return 1; }, + _ >> 0), + 1); + EXPECT_EQ( + match(b) + | on((is || is) >> [] { return 1; }, + _ >> 0), + 1); + EXPECT_EQ( + match(c) + | on((is || is) >> [] { return 1; }, + _ >> 0), + 0); +}