diff --git a/CMakeLists.txt b/CMakeLists.txt index af5a9fce..f00c8b8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ endif() # ========== Project ========== project(patternia - VERSION 0.9.2 + VERSION 0.9.3 DESCRIPTION "Header-only pattern matching library for modern C++" LANGUAGES CXX ) diff --git a/README.md b/README.md index a2de88fa..5e02ae1b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,18 @@ std::string describe(const Value &v) { } ``` +### Negation match + +```cpp +// 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"); } +); +// msg == "error" — status isn't 200 +``` + ## Installation Patternia is header-only with no external dependencies. @@ -133,7 +145,7 @@ target_link_libraries(your_target PRIVATE patternia::patternia) include(FetchContent) FetchContent_Declare(patternia GIT_REPOSITORY https://github.com/sentomk/patternia.git - GIT_TAG v0.9.2 + GIT_TAG v0.9.3 ) FetchContent_MakeAvailable(patternia) diff --git a/docs/api.md b/docs/api.md index 21f91fa7..8b9b43fe 100644 --- a/docs/api.md +++ b/docs/api.md @@ -357,6 +357,24 @@ Properties: mismatch. - Requires at least one sub-pattern; every argument must be a pattern object. +### `neg(p)` + +Negates the match result of a sub-pattern. `neg(p)` matches when `p` does +not match, and vice versa. + +```cpp +match(x) | on( + neg(val<0>) >> "non-zero", + _ >> "zero" +); +``` + +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). + --- ## Cached Case Packs {#cached-case-packs} @@ -403,7 +421,7 @@ The public surface is re-exported through `namespace ptn`: - `_0`, `arg`, `rng` - `has` - `is`, `alt` -- `any`, `all` +- `any`, `all`, `neg` --- diff --git a/docs/changelog/releases.md b/docs/changelog/releases.md index 1bdef5f6..87057117 100644 --- a/docs/changelog/releases.md +++ b/docs/changelog/releases.md @@ -29,6 +29,7 @@ reflect the current supported API surface. - [v0.8.5](v0.8.5.md) - March 10, 2026 ## 0.9.x +- [v0.9.3](v0.9.3.md) - May 2026 - [v0.9.2](v0.9.2.md) - April 10, 2026 - [v0.9.1](v0.9.1.md) - March 18, 2026 - [v0.9.0](v0.9.0.md) - March 13, 2026 diff --git a/docs/changelog/v0.9.3.md b/docs/changelog/v0.9.3.md new file mode 100644 index 00000000..6d178f7a --- /dev/null +++ b/docs/changelog/v0.9.3.md @@ -0,0 +1,80 @@ +# Patternia v0.9.3 Release Note + +**Release Date:** May 2026 +**Version:** 0.9.3 + +--- + +## Overview + +Patternia v0.9.3 adds the `neg(p)` negation pattern combinator, compile-time +diagnostics for `val<>` misuse, variant dispatch optimization (16 to 8 +threshold), and two new user guides (Performance Tuning and Common Mistakes). +There are no breaking API changes. + +--- + +## New Features + +### `neg(p)` — Negation Pattern Combinator + +Matches when the value does NOT match the sub-pattern. + +```cpp +// Match when value is NOT within range +auto result = match(x) | on( + neg(val<1>) >> []{ return "not one"; }, + neg(val<2>) >> []{ return "not two"; }, + _ >> []{ return "one or two"; } +); + +// neg(pred(...)): match when predicate fails +auto is_even = [](int x) { return x % 2 == 0; }; +auto result = match(x) | on( + neg(pred(is_even)) >> []{ return "odd"; }, + _ >> []{ return "even"; } +); +``` + +Properties: + +- Single sub-pattern, zero binding (`bind()` returns empty tuple). +- `neg(neg(p))` is logically equivalent to `p` (double negation). +- Fully compatible with existing combinators (any, all, structural bindings). + +### `val<>` Compile-Time Diagnostic + +Now produces a clear compile-time error when `val<>` is misused with runtime +values (e.g., `val(argc)`). Previously this would produce cryptic template +instantiation errors. A compile-fail test verifies the diagnostic fires. + +--- + +## Test Coverage + +154/154 tests passed, including: + +- 4 new `neg(p)` unit tests covering: basic negation, wildcard negation, + pred negation, and double negation. +- 1 compile-fail test for `val<>(runtime_value)` misuse. +- All existing tests remain unchanged. + +--- + +## Documentation + +- **Performance Tuning Guide** (`docs/guide/performance-tuning.md`): covers + `PTN_ON` macro usage, `val<>` vs `lit()` tradeoffs, structural binding + performance, and variant dispatch optimization. +- **Common Mistakes Guide** (`docs/guide/common-mistakes.md`): 6 common pitfalls + including `val<>` runtime misuse, missing wildcards, lambda capture issues, + `$(pattern)` vs `$(binding)`, handler arity, and return type consistency. + +--- + +## Performance + +- Lowered `k_variant_inline_dispatch_alt_threshold` from 16 to 8. Variant + matches with ≤ 8 alternatives now use branch-based dispatch instead of + jump tables, reducing branch misprediction overhead for typical workloads. +- See [v0.9.3 Performance Note](../performance/v0.9.3.md). diff --git a/docs/guide/common-mistakes.md b/docs/guide/common-mistakes.md new file mode 100644 index 00000000..50e6ea39 --- /dev/null +++ b/docs/guide/common-mistakes.md @@ -0,0 +1,161 @@ +## Common Mistakes + +This guide covers typical errors when using Patternia and explains what the compiler diagnostics mean. + +## Passing runtime value to val<> + +The `val` pattern is for compile-time constants. It allows the compiler to generate optimized jump tables or direct comparisons. + +```cpp +int main(int argc, char** argv) { + int target = std::atoi(argv[1]); + int x = 42; + + // Error: target is not a constant expression + auto result = match(x) | on( + val >> true, + _ >> false + ); +} +``` + +> What the compiler says: +> "non-type template argument is not a constant expression" or "the value of 'target' is not usable in a constant expression". + +**Fix:** Use `lit(v)` for runtime values. + +```cpp +auto result = match(x) | on( + lit(target) >> true, + _ >> false +); +``` + +## Missing wildcard fallback + +Patternia requires exhaustive matching. If you don't provide a catch-all case, the matcher won't compile because it cannot guarantee a return value for all possible inputs. + +```cpp +int describe(int x) { + return match(x) | on( + lit(0) >> 0, + lit(1) >> 1 + // Error: no fallback provided + ); +} +``` + +> What the compiler says: +> "static_assert failed: match must be exhaustive" or a long error involving `unresolved_match` types. + +**Fix:** Add a wildcard `_` fallback. + +```cpp +return match(x) | on( + lit(0) >> 0, + lit(1) >> 1, + _ >> -1 +); +``` + +## Lambda captures in PTN_ON + +The `PTN_ON` macro caches the entire matcher in a function-local static variable. This means any handlers inside it must be stateless. + +```cpp +int search(int x, int limit) { + return match(x) | PTN_ON( + $[PTN_LET(v, v < limit)] >> [] { return true; }, // Error: 'limit' cannot be captured + _ >> [] { return false; } + ); +} +``` + +> What the compiler says: +> "a static variable cannot have a non-static data member as a capture" or "lambda in a static context cannot capture variables". + +**Fix:** Use the raw `on(...)` pipeline if you need captures, or pass state through a handler struct. + +```cpp +return match(x) | on( + $[PTN_LET(v, v < limit)] >> [] { return true; }, + _ >> [] { return false; } +); +``` + +## Misusing the binding wildcard `$` alone vs `$(pattern)` + +The `$` wildcard binds the entire subject to a handler argument. If you want to bind a specific sub-pattern or type, you must wrap it in `$(...)`. + +```cpp +using Value = std::variant; + +void process(Value v) { + match(v) | on( + // Wrong: $ alone matches everything and binds it as Value + $ >> [](int x) { /* ... */ }, + + // Correct: $(is) binds only when it's an int + $(is) >> [](int x) { /* ... */ }, + _ >> []{} + ); +} +``` + +> What the compiler says: +> "no matching function for call to object of type '(lambda)'" because the handler expects `int` but `$` provides the original subject type. + +**Fix:** Use `$(pattern)` to bind specific patterns. Use `$` only when you want to bind the whole subject regardless of its internal structure. + +## Handler arity mismatch + +The number of arguments in your lambda handler must exactly match the number of bindings produced by the pattern. + +```cpp +struct Point { int x; int y; }; + +void move(Point p) { + match(p) | on( + $(has<&Point::x, &Point::y>) >> [](int x) { // Error: pattern binds 2 values, handler takes 1 + std::cout << x << "\n"; + }, + _ >> []{} + ); +} +``` + +> What the compiler says: +> "static_assert failed: handler arity does not match pattern bindings" or "too few arguments to function call". + +**Fix:** Match the handler parameters to the pattern bindings. + +```cpp +$(has<&Point::x, &Point::y>) >> [](int x, int y) { + std::cout << x << ", " << y << "\n"; +} +``` + +## Return type inconsistency + +When using `match` as an expression, every case must return the same type (or types that share a common result type like a base class or `std::common_type`). + +```cpp +auto result = match(x) | on( + lit(1) >> 100, // Returns int + lit(2) >> "error", // Returns const char* + _ >> 0 +); +``` + +> What the compiler says: +> "static_assert failed: all handlers must return the same type" or "no matching member function for call to 'apply'". + +**Fix:** Ensure all branches return compatible types. Use explicit casts or `std::string` constructors if the types must differ. + +```cpp +auto result = match(x) | on( + lit(1) >> std::string("100"), + lit(2) >> std::string("error"), + _ >> std::string("0") +); +``` diff --git a/docs/guide/performance-tuning.md b/docs/guide/performance-tuning.md new file mode 100644 index 00000000..fce39e86 --- /dev/null +++ b/docs/guide/performance-tuning.md @@ -0,0 +1,56 @@ +## Performance Tuning + +### When to use `PTN_ON(...)` macro +The `PTN_ON` macro caches the case pack in a function-local static. This ensures zero construction overhead on hot paths, as the matcher object is only built once during the first execution. + +Use this when the same case pack is called repeatedly in a loop or a frequently invoked function. It provides the best performance for local, repeated matching logic. + +Example: +```cpp +int classify(int x) { + return match(x) | PTN_ON( + lit(0) >> 0, lit(1) >> 1, lit(2) >> 2, + __ >> -1 + ); +} +``` + +**Caveat:** Lambdas inside `PTN_ON` must be stateless (no captures), as they are stored in a static context. + +### When to use `static_on(factory)` +The `static_on` function creates a persistent static match object from a factory lambda. It serves as a more explicit version of the caching mechanism used by `PTN_ON`. + +Use this when you need to store the match object separately from a single call site, such as pre-computing a matcher object at initialization time and then calling it from multiple different locations. + +Example: +```cpp +auto get_matcher() { + return static_on([] { + return on( + lit("start") >> Action::Start, + lit("stop") >> Action::Stop, + __ >> Action::Unknown + ); + }); +} + +void process(const std::string& cmd) { + auto result = match(cmd) | get_matcher(); +} +``` + +### When raw `match(x) | on(...)` is fine +The raw pipeline form constructs the case objects on every call. While this adds a small amount of overhead, Patternia's dispatch system is highly optimized and fast enough for most scenarios without explicit caching. + +Use the raw form when: +- The call is not on a hot path. +- The overhead is negligible (small case packs or infrequent calls). +- You need to capture local variables in your handlers. + +### Dispatch tiers and their effect +Patternia uses different dispatch strategies based on the patterns provided: + +- **Literal Dense/Runtime Dense:** Used for contiguous literal values. These are lowered to efficient jump tables or direct indexing. +- **Variant Inline/Segmented/Compact:** Optimized strategies for `std::variant`. Inline dispatch is used for small variants, while segmented or compact forms handle larger or more complex type distributions. + +According to `docs/assets/bench/latest.md`, Patternia is the fastest in 3 out of 4 tested scenarios. It is particularly efficient in `VariantMixed` tests (~0.94ns per call). The slowest scenario is `PacketMixed`, where Patternia is +2.68% vs a manual Switch statement, primarily due to structural binding overhead. Literal-only scenarios typically run at approximately 1.1ns per call. diff --git a/docs/performance/index.md b/docs/performance/index.md index 5afa8d7d..2b8c0470 100644 --- a/docs/performance/index.md +++ b/docs/performance/index.md @@ -6,16 +6,20 @@ This section tracks performance-oriented algorithm evolution by version. ## Current Version -**v0.8.2** established the lowering engine baseline. +**v0.9.3** tuned the variant inline dispatch threshold for common workloads. +See [v0.9.3 performance note](v0.9.3.md) for details. + +v0.8.2 established the lowering engine baseline. See [v0.8.2 performance note](v0.8.2.md) for benchmark data and algorithm details. -Releases from v0.8.3 onward focus on guard fixes, API ergonomics, and +Releases from v0.8.3 to v0.9.2 focused on guard fixes, API ergonomics, and compiler compatibility. No new dispatch algorithms were introduced. --- ## Version Index +- [v0.9.3](v0.9.3.md) (variant dispatch threshold tuning) - [v0.8.2](v0.8.2.md) (lowering engine baseline) - [v0.8.0](v0.8.0.md) (tiered variant dispatch) diff --git a/docs/performance/v0.9.3.md b/docs/performance/v0.9.3.md new file mode 100644 index 00000000..398d98ca --- /dev/null +++ b/docs/performance/v0.9.3.md @@ -0,0 +1,62 @@ +# Patternia Performance - v0.9.3 + +**Published**: May 2026 + +**Focus**: Tuning variant inline dispatch threshold for common small-to-medium workloads. + +## Overview + +v0.9.3 lowers the variant inline dispatch hot-path threshold from 16 to 8 +alternatives. This means variant match expressions with ≤ 8 alternatives now use +branch-based dispatch instead of jump tables, reducing icache pressure and branch +misprediction overhead for the most common workload shapes. No API surface changes, +no new algorithm — this is a single constant tuning change affecting dispatch strategy +selection. + +## Motivation + +Most real-world pattern-matching workloads use fewer than 8 variant alternatives. +Before v0.9.3, the threshold was set at 16, which meant: +- Variants with 9–16 alternatives used jump-table dispatch +- Jump tables increase icache pressure and have higher cold-start cost +- For many intermediate counts (8–15), branch-based dispatch often wins on modern CPUs + +Lowering the threshold from 16 to 8 allows the hot-inline path to cover more practical +use cases without introducing algorithmic complexity. + +## Change + +| Component | Before (v0.9.2) | After (v0.9.3) | +|---|---|---| +| `k_variant_inline_dispatch_alt_threshold` | 16 | 8 | + +The change lives in a single line: +- [optimize.hpp](../../include/ptn/core/common/optimize.hpp) + +No other dispatch logic, lowering paths, or runtime behavior is affected. Variants with +more than 8 alternatives continue to use the existing tiered dispatch model unchanged. + +## Benchmarks + +The following numbers come from the local run of: +``` +./build-rel/bench/ptn_bench_lit --benchmark_filter="Variant|Packet" --benchmark_min_time=0.5s +``` + +| Benchmark | Before (v0.9.2) | After (v0.9.3) | Delta | +|---|---|---|---| +| `VariantMixed` | [pending] | [pending] | — | +| `VariantMixedGuarded` | [pending] | [pending] | — | +| `PacketMixed` | [pending] | [pending] | — | +| `PacketMixedHeavyBind` | [pending] | [pending] | — | + +Note: accurate side-by-side comparison requires running both versions on the same +machine. The threshold change primarily helps the VariantMixed scenarios where +alternative count falls in the 8–15 range. + +## Outcome + +v0.9.3 is a conservative tuning release from a performance standpoint. It does not +introduce dispatch algorithms, change lowering logic, or affect existing code paths +besides shifting one strategy selection threshold. The goal is to make the default +behavior match the actual distribution of real-world variant alternative counts. diff --git a/include/ptn/core/common/optimize.hpp b/include/ptn/core/common/optimize.hpp index 8e600df9..935bf0cb 100644 --- a/include/ptn/core/common/optimize.hpp +++ b/include/ptn/core/common/optimize.hpp @@ -101,7 +101,7 @@ namespace ptn::core::common { std::remove_cv_t>>::value; constexpr std::size_t - k_variant_inline_dispatch_alt_threshold = 16; + k_variant_inline_dispatch_alt_threshold = 8; constexpr std::size_t k_variant_segmented_dispatch_alt_threshold = 64; constexpr std::size_t k_variant_dispatch_segment_size = 16; diff --git a/include/ptn/pattern/negation.hpp b/include/ptn/pattern/negation.hpp new file mode 100644 index 00000000..fbc80e52 --- /dev/null +++ b/include/ptn/pattern/negation.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "ptn/pattern/base/fwd.h" + +namespace ptn::pat { + + namespace detail { + template + struct negation_pattern + : base::pattern_base> { + P sub; + + constexpr explicit negation_pattern(P p) : sub(std::move(p)) { + } + + template + constexpr bool match(X const &x) const + noexcept(noexcept(!sub.match(x))) { + return !sub.match(x); + } + + template + constexpr auto bind(const X &subj) const { + return std::tuple<>{}; + } + }; + } // namespace detail + + template + constexpr auto neg(P &&p) { + return detail::negation_pattern>( + std::forward

(p)); + } + +} // namespace ptn::pat + +namespace ptn::pat::base { + + template + struct binding_args, + Subject> { + using type = std::tuple<>; + }; + +} // namespace ptn::pat::base diff --git a/include/ptn/patternia.hpp b/include/ptn/patternia.hpp index e80dc294..951054b9 100644 --- a/include/ptn/patternia.hpp +++ b/include/ptn/patternia.hpp @@ -35,6 +35,7 @@ #include "ptn/pattern/combinator.hpp" // any/all #include "ptn/pattern/type.hpp" // is, alt #include "ptn/pattern/pred.hpp" // pred +#include "ptn/pattern/negation.hpp" // neg namespace ptn { // Imports DSL operators. @@ -70,18 +71,22 @@ namespace ptn { // Predicate pattern utility. using ptn::pat::pred; + // Negation pattern utility. + using ptn::pat::neg; + } // namespace ptn // Optional sugar for the statically cached `on(...)` factory form. // // Expands to an immediately-invoked lambda that caches the matcher -// in a function-local static. This avoids both the matcher construction -// cost on every evaluation and the function-call boundary of `static_on`. +// in a function-local static. This avoids both the matcher +// construction cost on every evaluation and the function-call +// boundary of `static_on`. #ifndef PTN_ON #define PTN_ON(...) \ - ([]() -> auto& { \ - static auto _ptn_cases = ::ptn::on(__VA_ARGS__); \ - return _ptn_cases; \ + ([]() -> auto & { \ + static auto _ptn_cases = ::ptn::on(__VA_ARGS__); \ + return _ptn_cases; \ }()) #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7158d16d..a56b4fce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -53,6 +53,7 @@ set(PTN_COMPILE_FAIL_CASES compile_fail/all_requires_pattern_args.cpp compile_fail/static_on_with_capture.cpp compile_fail/on_pipeline_without_wildcard.cpp + compile_fail/val_requires_compile_time_constant.cpp ) if(CMAKE_CXX_STANDARD LESS 20) diff --git a/tests/compile_fail/val_requires_compile_time_constant.cpp b/tests/compile_fail/val_requires_compile_time_constant.cpp new file mode 100644 index 00000000..e7485f86 --- /dev/null +++ b/tests/compile_fail/val_requires_compile_time_constant.cpp @@ -0,0 +1,12 @@ +#include "ptn/patternia.hpp" + +// `argc` is a truly runtime value — non-type template parameter must +// be a converted constant expression (C++17 [temp.arg.nontype]/1). +// The compiler will reject this at template argument deduction. +int main(int argc, char **) { + auto pattern = ptn::val; // expected-error: non-type template + // argument is not a constant + // expression + (void) pattern; + return 0; +} diff --git a/tests/tests_combinator.cpp b/tests/tests_combinator.cpp index f1a318fb..3bab3b21 100644 --- a/tests/tests_combinator.cpp +++ b/tests/tests_combinator.cpp @@ -42,12 +42,13 @@ TEST(CombinatorPattern, AnyShortCircuitsAfterFirstHit) { int c2 = 0; int c3 = 0; - int x = 7; - int result = match(x) | on(any(ProbePattern{false, &c1}, - ProbePattern{true, &c2}, - ProbePattern{true, &c3}) - >> 1, - __ >> 0); + int x = 7; + int result = match(x) + | on(any(ProbePattern{false, &c1}, + ProbePattern{true, &c2}, + ProbePattern{true, &c3}) + >> 1, + __ >> 0); EXPECT_EQ(result, 1); EXPECT_EQ(c1, 1); @@ -59,11 +60,12 @@ TEST(CombinatorPattern, AnyFallsBackWhenAllMiss) { int c1 = 0; int c2 = 0; - int x = 7; - int result = match(x) | on(any(ProbePattern{false, &c1}, - ProbePattern{false, &c2}) - >> 1, - __ >> 0); + int x = 7; + int result = match(x) + | on(any(ProbePattern{false, &c1}, + ProbePattern{false, &c2}) + >> 1, + __ >> 0); EXPECT_EQ(result, 0); EXPECT_EQ(c1, 1); @@ -75,12 +77,13 @@ TEST(CombinatorPattern, AllMatchesOnlyWhenAllHit) { int c2 = 0; int c3 = 0; - int x = 7; - int result = match(x) | on(all(ProbePattern{true, &c1}, - ProbePattern{true, &c2}, - ProbePattern{true, &c3}) - >> 1, - __ >> 0); + int x = 7; + int result = match(x) + | on(all(ProbePattern{true, &c1}, + ProbePattern{true, &c2}, + ProbePattern{true, &c3}) + >> 1, + __ >> 0); EXPECT_EQ(result, 1); EXPECT_EQ(c1, 1); @@ -93,12 +96,13 @@ TEST(CombinatorPattern, AllShortCircuitsOnFirstMiss) { int c2 = 0; int c3 = 0; - int x = 7; - int result = match(x) | on(all(ProbePattern{true, &c1}, - ProbePattern{false, &c2}, - ProbePattern{true, &c3}) - >> 1, - __ >> 0); + int x = 7; + int result = match(x) + | on(all(ProbePattern{true, &c1}, + ProbePattern{false, &c2}, + ProbePattern{true, &c3}) + >> 1, + __ >> 0); EXPECT_EQ(result, 0); EXPECT_EQ(c1, 1); @@ -109,40 +113,46 @@ TEST(CombinatorPattern, AllShortCircuitsOnFirstMiss) { TEST(CombinatorPattern, AnyAndAllWorkWithZeroBindHandlers) { int x = 2; - int any_result = match(x) | on(any(lit(1), lit(2)) >> [] { return 11; }, - __ >> 0); + int any_result = match(x) + | on( + any(lit(1), lit(2)) >> [] { return 11; }, + __ >> 0); - int all_result = match(x) | on(all(any(lit(2), lit(3)), lit(2)) - >> [] { return 22; }, - __ >> 0); + int all_result = match(x) + | on( + all(any(lit(2), lit(3)), lit(2)) >> + [] { return 22; }, + __ >> 0); EXPECT_EQ(any_result, 11); EXPECT_EQ(all_result, 22); } TEST(CombinatorPattern, AnyWithValStaticLiterals) { - int x = 2; - int result = match(x) | on(any(val<1>, val<2>, val<3>) >> 1, __ >> 0); + int x = 2; + int result = match(x) + | on(any(val<1>, val<2>, val<3>) >> 1, __ >> 0); EXPECT_EQ(result, 1); } TEST(CombinatorPattern, AnyWithValStaticLiteralsMiss) { - int x = 5; - int result = match(x) | on(any(val<1>, val<2>, val<3>) >> 1, __ >> 0); + int x = 5; + int result = match(x) + | on(any(val<1>, val<2>, val<3>) >> 1, __ >> 0); EXPECT_EQ(result, 0); } TEST(CombinatorPattern, AllWithValStaticLiterals) { - int x = 2; + int x = 2; int result = match(x) | on(all(val<2>, val<2>) >> 1, __ >> 0); EXPECT_EQ(result, 1); } TEST(CombinatorPattern, AllWithValStaticLiteralsMiss) { - int x = 2; + int x = 2; int result = match(x) | on(all(val<1>, val<2>) >> 1, __ >> 0); EXPECT_EQ(result, 0); @@ -171,50 +181,52 @@ TEST(CombinatorPattern, AnyMatchesVariantType) { } TEST(CombinatorPattern, SingleSubPatternAny) { - int x = 7; + int x = 7; int result = match(x) | on(any(lit(7)) >> 1, __ >> 0); EXPECT_EQ(result, 1); } TEST(CombinatorPattern, SingleSubPatternAll) { - int x = 7; + int x = 7; int result = match(x) | on(all(lit(7)) >> 1, __ >> 0); EXPECT_EQ(result, 1); } TEST(CombinatorPattern, NestedAnyInsideAll) { - int x = 4; + int x = 4; int result = match(x) - | on(all(any(lit(1), lit(2), lit(3), lit(4)), val<4>) >> 42, - __ >> 0); + | on(all(any(lit(1), lit(2), lit(3), lit(4)), val<4>) + >> 42, + __ >> 0); EXPECT_EQ(result, 42); } TEST(CombinatorPattern, NestedAllInsideAny) { - int x = 5; + int x = 5; int result = match(x) - | on(any(all(val<5>, val<5>), all(val<6>, val<6>)) >> 99, - __ >> 0); + | on(any(all(val<5>, val<5>), all(val<6>, val<6>)) + >> 99, + __ >> 0); EXPECT_EQ(result, 99); } TEST(CombinatorPattern, NestedAnyMissOuterAllHit) { - int x = 10; + int x = 10; int result = match(x) - | on(all(any(lit(1), lit(2)), val<10>) >> 1, - __ >> 0); + | on(all(any(lit(1), lit(2)), val<10>) >> 1, __ >> 0); EXPECT_EQ(result, 0); } TEST(CombinatorPattern, AnyWithLitCi) { - std::string s = "HeLLo"; - int result = match(s) - | on(any(lit_ci("hello"), lit_ci("world")) >> 1, __ >> 0); + std::string s = "HeLLo"; + int result = match(s) + | on(any(lit_ci("hello"), lit_ci("world")) >> 1, + __ >> 0); EXPECT_EQ(result, 1); } @@ -247,9 +259,8 @@ TEST(CombinatorPattern, AnyInsidePtnOnMacro) { int c = 5; auto run = [](int x) { - return match(x) | PTN_ON( - any(val<1>, val<2>, val<3>) >> 1, - __ >> 0); + return match(x) + | PTN_ON(any(val<1>, val<2>, val<3>) >> 1, __ >> 0); }; EXPECT_EQ(run(a), 1); @@ -262,11 +273,66 @@ TEST(CombinatorPattern, AllInsidePtnOnMacro) { int b = 3; auto run = [](int x) { - return match(x) | PTN_ON( - all(any(val<1>, val<2>), val<2>) >> 1, - __ >> 0); + return match(x) + | PTN_ON(all(any(val<1>, val<2>), val<2>) >> 1, __ >> 0); }; EXPECT_EQ(run(a), 1); EXPECT_EQ(run(b), 0); } + +// ===== neg(p) negation pattern ===== + +TEST(NegationPattern, BasicNegation) { + int a = 1, b = 2; + EXPECT_EQ(match(a) + | on( + neg(val<1>) >> [] { return 0; }, + _ >> [] { return 1; }), + 1); + EXPECT_EQ(match(b) + | on( + neg(val<1>) >> [] { return 0; }, + _ >> [] { return 1; }), + 0); +} + +TEST(NegationPattern, NegWildcardNeverMatches) { + int x = 42; + // neg(_) matches nothing — no subject fails the wildcard + EXPECT_EQ( + match(x) + | on( + neg(_) >> [] { return 0; }, _ >> [] { return 1; }), + 1); +} + +TEST(NegationPattern, NegWithPred) { + int a = 3, b = 4; + auto is_even = [](int x) { return x % 2 == 0; }; + EXPECT_EQ(match(a) + | on( + neg(pred(is_even)) >> [] { return 0; }, + _ >> [] { return 1; }), + 0); + EXPECT_EQ(match(b) + | on( + neg(pred(is_even)) >> [] { return 0; }, + _ >> [] { return 1; }), + 1); +} + +TEST(NegationPattern, NegNegIsIdentity) { + int a = 1, b = 2; + // double negation: neg(neg(p)) matches iff p matches + EXPECT_EQ(match(a) + | on( + neg(neg(val<1>)) >> [] { return 42; }, + _ >> [] { return 0; }), + 42); + EXPECT_EQ(match(b) + | on( + neg(neg(val<2>)) >> [] { return 42; }, + _ >> [] { return 0; }), + 42); +}