Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...)`

Expand All @@ -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)`

Expand All @@ -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).

---

Expand Down
10 changes: 10 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
65 changes: 57 additions & 8 deletions include/ptn/pattern/combinator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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 <typename... Patterns>
struct any_pattern : base::pattern_base<any_pattern<Patterns...>> {
struct any_pattern
: base::pattern_base<any_pattern<Patterns...>> {
std::tuple<Patterns...> patterns;

template <typename... Ps,
typename = std::enable_if_t<sizeof...(Ps) == sizeof...(Patterns)>>
typename = std::enable_if_t<sizeof...(Ps)
== sizeof...(Patterns)>>
constexpr explicit any_pattern(Ps &&...ps)
: patterns(std::forward<Ps>(ps)...) {
}
Expand All @@ -49,11 +52,13 @@ namespace ptn::pat {
// Matches only when every sub-pattern matches. Like any_pattern,
// this combinator is non-binding.
template <typename... Patterns>
struct all_pattern : base::pattern_base<all_pattern<Patterns...>> {
struct all_pattern
: base::pattern_base<all_pattern<Patterns...>> {
std::tuple<Patterns...> patterns;

template <typename... Ps,
typename = std::enable_if_t<sizeof...(Ps) == sizeof...(Patterns)>>
typename = std::enable_if_t<sizeof...(Ps)
== sizeof...(Patterns)>>
constexpr explicit all_pattern(Ps &&...ps)
: patterns(std::forward<Ps>(ps)...) {
}
Expand Down Expand Up @@ -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<base::pattern_tag, std::decay_t<Patterns>> && ...),
(std::is_base_of_v<base::pattern_tag, std::decay_t<Patterns>>
&& ...),
"[Patternia.any]: every argument must be a pattern object.");

return detail::any_pattern<std::decay_t<Patterns>...>(
Expand All @@ -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<base::pattern_tag, std::decay_t<Patterns>> && ...),
(std::is_base_of_v<base::pattern_tag, std::decay_t<Patterns>>
&& ...),
"[Patternia.all]: every argument must be a pattern object.");

return detail::all_pattern<std::decay_t<Patterns>...>(
Expand All @@ -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 <typename L, typename R>
inline constexpr bool pattern_pair_v =
std::is_base_of_v<pattern_tag, std::decay_t<L>>
&& std::is_base_of_v<pattern_tag, std::decay_t<R>>
&& !pat::traits::is_guard_predicate_v<std::decay_t<L>>
&& !pat::traits::is_guard_predicate_v<std::decay_t<R>>;

} // 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 <typename L,
typename R,
std::enable_if_t<detail::pattern_pair_v<L, R>, int> = 0>
constexpr auto operator||(L &&l, R &&r) {
return pat::any(std::forward<L>(l), std::forward<R>(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 <typename L,
typename R,
std::enable_if_t<detail::pattern_pair_v<L, R>, int> = 0>
constexpr auto operator&&(L &&l, R &&r) {
return pat::all(std::forward<L>(l), std::forward<R>(r));
}

template <typename... Patterns, typename Subject>
struct binding_args<ptn::pat::detail::any_pattern<Patterns...>, Subject> {
struct binding_args<ptn::pat::detail::any_pattern<Patterns...>,
Subject> {
using type = std::tuple<>;
};

template <typename... Patterns, typename Subject>
struct binding_args<ptn::pat::detail::all_pattern<Patterns...>, Subject> {
struct binding_args<ptn::pat::detail::all_pattern<Patterns...>,
Subject> {
using type = std::tuple<>;
};

Expand Down
20 changes: 20 additions & 0 deletions include/ptn/pattern/negation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <utility>

#include "ptn/pattern/base/fwd.h"
#include "ptn/pattern/base/pattern_base.hpp"
#include "ptn/pattern/base/pattern_traits.hpp"

namespace ptn::pat {

Expand Down Expand Up @@ -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<pattern_tag, std::decay_t<P>>
&& !pat::traits::is_guard_predicate_v<std::decay_t<P>>,
int> = 0>
constexpr auto operator!(P &&p) {
return pat::neg(std::forward<P>(p));
}

} // namespace ptn::pat::base
85 changes: 85 additions & 0 deletions tests/tests_combinator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<decltype(!lit(1)), decltype(neg(lit(1)))>);
}

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<int, std::string, double> a = 1;
std::variant<int, std::string, double> b = std::string("s");
std::variant<int, std::string, double> c = 1.5;
EXPECT_EQ(
match(a)
| on((is<int> || is<std::string>) >> [] { return 1; },
_ >> 0),
1);
EXPECT_EQ(
match(b)
| on((is<int> || is<std::string>) >> [] { return 1; },
_ >> 0),
1);
EXPECT_EQ(
match(c)
| on((is<int> || is<std::string>) >> [] { return 1; },
_ >> 0),
0);
}
Loading