Skip to content

Patternia v0.9.4 Release Note

Release Date: August 2026 Version: 0.9.4


Overview

Patternia v0.9.4 is a guard-focused release. It introduces PTN_BIND member-anchored named placeholders for structural guards, unifies the placeholder syntax around _ and PTN_BIND, and adds operator sugar (!p, (a || b), (a && b)) for the pattern combinators.

This release contains breaking changes; see Breaking Changes and the Migration Guide below.


Breaking Changes

Positional guard APIs removed

The positional guard spellings are removed in favor of _ and PTN_BIND:

Removed Replacement
__ (double-underscore) _
_0 _
arg<N> PTN_BIND names
PTN_LET(v, expr) PTN_BIND + guard expression
PTN_WHERE((a, b), expr) PTN_BIND + guard expression

_ now serves both as the single-value guard placeholder and as the wildcard fallback.

PTN_BIND names must be members of the type

PTN_BIND(Type, names...) expands each name to member_t<&Type::name>. Placeholder names that were aliases rather than real members of Type no longer compile; use the member names themselves. Misspellings are now caught at the PTN_BIND line.


New Features

PTN_BIND(Type, names...) — named guard placeholders

Declares member-anchored named placeholders for guards attached to has<...>, supporting one to ten names:

struct Point { int x; int y; };
PTN_BIND(Point, x, y);

match(p) | on(
  $(has<&Point::x, &Point::y>)[x * x + y * y == 25] >> "on circle",
  _ >> "elsewhere"
);

Names follow members, not positions: each name resolves to the position of its member in the has<...> member list at compile time, so the order of member pointers in has<...> does not matter:

$(has<&Point::y, &Point::x>)[x == 3 && y == 4]  // x is still .x

Misuse is caught at compile time: a misspelled member name fails at the PTN_BIND line, and a name whose member is not listed in has<...> fails a static_assert.

Declarations are valid at namespace or block scope. Block-scope declarations use static storage duration, so they can be referenced inside PTN_ON's captureless caching lambda:

int classify(const Point &p) {
  PTN_BIND(Point, x, y);
  return match(p) | PTN_ON(
      $(has<&Point::x, &Point::y>)[x * x + y * y == 25] >> 1,
      _ >> 0);
}

Operator sugar for combinators

The pattern algebra now has operator forms:

match(status) | on(
  !val<200>                  >> "error",     // !p     == neg(p)
  (lit(301) || lit(302))     >> "redirect",  // (a||b) == any(a, b)
  (pred(is_json) && !lit(0)) >> "json",      // (a&&b) == all(a, b)
  _                          >> "ok"
);

The operators only accept pattern operands, so guard-level && / || inside [...] keep their existing meaning. Note that >> binds tighter than || and &&, so combined patterns need parentheses in a case; unary ! needs none.


Internal Improvements

  • CI: commit-style PR validation, clang-format check, LLVM source-based coverage, and a fix letting docs-only PRs satisfy branch protection.
  • Bench: compile-time translation units are generated into the build tree instead of the source tree.
  • Docs: contributing guide now documents the CI-enforced PR rules.

Migration Guide

PTN_LET / PTN_WHERE

// v0.9.3
auto g1 = PTN_LET(v, v == 1);
auto g2 = PTN_WHERE((x, y), x < y);

// v0.9.4 — single bound value: use `_`
$[_ == 1] >> ...;

// v0.9.4 — multiple bound values: declare names once
PTN_BIND(Point, x, y);
$(has<&Point::x, &Point::y>)[x < y] >> ...;

arg<N> / _0 / __

// v0.9.3
$[_0 > 0] >> ...;
$(has<&Point::x, &Point::y>)[arg<0> < arg<1>] >> ...;

// v0.9.4
$[_ > 0] >> ...;
PTN_BIND(Point, x, y);
$(has<&Point::x, &Point::y>)[x < y] >> ...;

Alias-style PTN_BIND names

// main-between-releases only (never in a tagged release):
PTN_BIND(Point, point_x, point_y);   // alias names

// v0.9.4 — names must be members of Point:
PTN_BIND(Point, x, y);