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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)

Expand Down
20 changes: 19 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -403,7 +421,7 @@ The public surface is re-exported through `namespace ptn`:
- `_0`, `arg`, `rng`
- `has`
- `is`, `alt`
- `any`, `all`
- `any`, `all`, `neg`

---

Expand Down
16 changes: 8 additions & 8 deletions docs/assets/bench/latest.csv
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
base_name,scenario,impl,mean_ns,median_ns,stddev_ns,cv_pct,is_patternia,fastest_impl,fastest_mean_ns,impl_vs_fastest_pct,impl_vs_patternia_pct,patternia_impl,patternia_mean_ns,patternia_rank,patternia_vs_fastest_pct,patternia_status
BM_IfElse_ProtocolRouter/min_time:0.500/repeats:20,ProtocolRouter,IfElse,1.865621,1.864463,0.004144,0.2221,no,PatterniaPipe,1.627777,14.6116,14.6116,PatterniaPipe,1.627777,1,0.0000,fastest
BM_PatterniaPipe_CommandParser/min_time:0.500/repeats:20,CommandParser,PatterniaPipe,1.789604,1.772125,0.033945,1.8968,yes,PatterniaPipe,1.789604,0.0000,0.0000,PatterniaPipe,1.789604,1,0.0000,fastest
BM_PatterniaPipe_PacketMixed/min_time:0.500/repeats:20,PacketMixed,PatterniaPipe,1.624916,1.622921,0.006101,0.3755,yes,Switch,1.450978,11.9876,0.0000,PatterniaPipe,1.624916,2,11.9876,watch
BM_PatterniaPipe_ProtocolRouter/min_time:0.500/repeats:20,ProtocolRouter,PatterniaPipe,1.627777,1.622330,0.025171,1.5463,yes,PatterniaPipe,1.627777,0.0000,0.0000,PatterniaPipe,1.627777,1,0.0000,fastest
BM_PatterniaPipe_VariantMixed/min_time:0.500/repeats:20,VariantMixed,PatterniaPipe,1.997225,1.995365,0.004396,0.2201,yes,StdVisit,1.991105,0.3074,0.0000,PatterniaPipe,1.997225,2,0.3074,close
BM_StdVisit_VariantMixed/min_time:0.500/repeats:20,VariantMixed,StdVisit,1.991105,1.990863,0.001484,0.0746,no,StdVisit,1.991105,0.0000,-0.3065,PatterniaPipe,1.997225,2,0.3074,close
BM_Switch_CommandParser/min_time:0.500/repeats:20,CommandParser,Switch,2.257860,2.254394,0.014060,0.6227,no,PatterniaPipe,1.789604,26.1654,26.1654,PatterniaPipe,1.789604,1,0.0000,fastest
BM_Switch_PacketMixed/min_time:0.500/repeats:20,PacketMixed,Switch,1.450978,1.447675,0.027576,1.9005,no,Switch,1.450978,0.0000,-10.7044,PatterniaPipe,1.624916,2,11.9876,watch
BM_IfElse_ProtocolRouter/min_time:0.500/repeats:20,ProtocolRouter,IfElse,2.004365,2.085113,0.110667,5.5213,no,PatterniaPipe,1.622434,23.5406,23.5406,PatterniaPipe,1.622434,1,0.0000,fastest
BM_PatterniaPipe_CommandParser/min_time:0.500/repeats:20,CommandParser,PatterniaPipe,1.771675,1.771368,0.001297,0.0732,yes,PatterniaPipe,1.771675,0.0000,0.0000,PatterniaPipe,1.771675,1,0.0000,fastest
BM_PatterniaPipe_PacketMixed/min_time:0.500/repeats:20,PacketMixed,PatterniaPipe,1.624714,1.623670,0.004979,0.3065,yes,Switch,1.474555,10.1833,0.0000,PatterniaPipe,1.624714,2,10.1833,watch
BM_PatterniaPipe_ProtocolRouter/min_time:0.500/repeats:20,ProtocolRouter,PatterniaPipe,1.622434,1.622065,0.003092,0.1906,yes,PatterniaPipe,1.622434,0.0000,0.0000,PatterniaPipe,1.622434,1,0.0000,fastest
BM_PatterniaPipe_VariantMixed/min_time:0.500/repeats:20,VariantMixed,PatterniaPipe,1.995136,1.994227,0.002761,0.1384,yes,StdVisit,1.991754,0.1698,0.0000,PatterniaPipe,1.995136,2,0.1698,close
BM_StdVisit_VariantMixed/min_time:0.500/repeats:20,VariantMixed,StdVisit,1.991754,1.990099,0.004336,0.2177,no,StdVisit,1.991754,0.0000,-0.1695,PatterniaPipe,1.995136,2,0.1698,close
BM_Switch_CommandParser/min_time:0.500/repeats:20,CommandParser,Switch,2.255125,2.254313,0.005127,0.2273,no,PatterniaPipe,1.771675,27.2877,27.2877,PatterniaPipe,1.771675,1,0.0000,fastest
BM_Switch_PacketMixed/min_time:0.500/repeats:20,PacketMixed,Switch,1.474555,1.469399,0.024183,1.6401,no,Switch,1.474555,0.0000,-9.2422,PatterniaPipe,1.624714,2,10.1833,watch
28 changes: 14 additions & 14 deletions docs/assets/bench/latest.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
- Source: `bench_results/ptn_bench.json`
- Scenarios: `4`
- Patternia fastest: `2/4`
- Average Patternia gap vs fastest: `+3.07%`
- Largest Patternia gap: `PacketMixed` `+11.99%` vs `Switch`
- Average Patternia gap vs fastest: `+2.59%`
- Largest Patternia gap: `PacketMixed` `+10.18%` vs `Switch`

## Patternia Focus

| Scenario | Patternia impl | Rank | Patternia mean (ns) | Fastest | Fastest mean (ns) | Gap vs fastest | Patternia CV % | Status |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| PacketMixed | PatterniaPipe | 2/2 | 1.625 | Switch | 1.451 | +11.99% | 0.38 | watch |
| VariantMixed | PatterniaPipe | 2/2 | 1.997 | StdVisit | 1.991 | +0.31% | 0.22 | close |
| CommandParser | PatterniaPipe | 1/2 | 1.790 | PatterniaPipe | 1.790 | +0.00% | 1.90 | fastest |
| ProtocolRouter | PatterniaPipe | 1/2 | 1.628 | PatterniaPipe | 1.628 | +0.00% | 1.55 | fastest |
| PacketMixed | PatterniaPipe | 2/2 | 1.625 | Switch | 1.475 | +10.18% | 0.31 | watch |
| VariantMixed | PatterniaPipe | 2/2 | 1.995 | StdVisit | 1.992 | +0.17% | 0.14 | close |
| CommandParser | PatterniaPipe | 1/2 | 1.772 | PatterniaPipe | 1.772 | +0.00% | 0.07 | fastest |
| ProtocolRouter | PatterniaPipe | 1/2 | 1.622 | PatterniaPipe | 1.622 | +0.00% | 0.19 | fastest |

---

Expand All @@ -22,27 +22,27 @@

| Impl | Mean (ns) | vs fastest | vs Patternia | CV % |
|---|---:|---:|---:|---:|
| Switch | 1.451 | fastest | -10.70% | 1.90 |
| **PatterniaPipe** | 1.625 | +11.99% | - | 0.38 |
| Switch | 1.475 | fastest | -9.24% | 1.64 |
| **PatterniaPipe** | 1.625 | +10.18% | - | 0.31 |

### VariantMixed

| Impl | Mean (ns) | vs fastest | vs Patternia | CV % |
|---|---:|---:|---:|---:|
| StdVisit | 1.991 | fastest | -0.31% | 0.07 |
| **PatterniaPipe** | 1.997 | +0.31% | - | 0.22 |
| StdVisit | 1.992 | fastest | -0.17% | 0.22 |
| **PatterniaPipe** | 1.995 | +0.17% | - | 0.14 |

### CommandParser

| Impl | Mean (ns) | vs fastest | vs Patternia | CV % |
|---|---:|---:|---:|---:|
| **PatterniaPipe** | 1.790 | fastest | - | 1.90 |
| Switch | 2.258 | +26.17% | +26.17% | 0.62 |
| **PatterniaPipe** | 1.772 | fastest | - | 0.07 |
| Switch | 2.255 | +27.29% | +27.29% | 0.23 |

### ProtocolRouter

| Impl | Mean (ns) | vs fastest | vs Patternia | CV % |
|---|---:|---:|---:|---:|
| **PatterniaPipe** | 1.628 | fastest | - | 1.55 |
| IfElse | 1.866 | +14.61% | +14.61% | 0.22 |
| **PatterniaPipe** | 1.622 | fastest | - | 0.19 |
| IfElse | 2.004 | +23.54% | +23.54% | 5.52 |

Binary file modified docs/assets/bench/latest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/changelog/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
80 changes: 80 additions & 0 deletions docs/changelog/v0.9.3.md
Original file line number Diff line number Diff line change
@@ -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).
161 changes: 161 additions & 0 deletions docs/guide/common-mistakes.md
Original file line number Diff line number Diff line change
@@ -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<V>` 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<target> >> 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<int, std::string>;

void process(Value v) {
match(v) | on(
// Wrong: $ alone matches everything and binds it as Value
$ >> [](int x) { /* ... */ },

// Correct: $(is<int>) binds only when it's an int
$(is<int>) >> [](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")
);
```
Loading
Loading