regex-engine
The Trieste regex engine is a header-only C++20 NFA-based matcher in include/trieste/regex_engine.h. It compiles UTF-8 regexes with a shunting-yard + Thompson pipeline and simulates them with epoch-based state deduplication.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "regex-engine"
description: "The Trieste regex engine is a header-only C++20 NFA-based matcher in include/trieste/regex_engine.h. It compiles UTF-8 regexes with a shunting-yard + Thompson pipeline and simulates them with epoch-based state deduplication."
license: "MIT"
---
# Regex Engine Design
The Trieste regex engine is a header-only C++20 NFA-based matcher in
`include/trieste/regex_engine.h`. It compiles UTF-8 regexes with a
shunting-yard + Thompson pipeline and simulates them with epoch-based state
deduplication.
The engine started as an iregexp-focused implementation, but now includes
compatibility extensions used by Trieste parsers and rewrite helpers.
## Architecture Overview
The engine processes a regex in four phases:
```
regex string ──► postfix runes ──► NFA ──► finalization ──► simulation
(shunting-yard) (Thompson) (closures+bitmaps) (parallel)
```
1. **Shunting-yard** (`regexp_to_postfix_runes`): Converts regex text to a
postfix rune stream with explicit operators.
2. **Thompson's construction** (`postfix_to_nfa`): Walks the postfix sequence
and builds an NFA from `StateDef` nodes using a fragment stack.
3. **Finalization** (`precompute_epsilon_closures` + `finalize_states`):
Precomputes epsilon closures (with trivial-closure detection) and
populates per-state ASCII acceptance bitmaps.
4. **Parallel simulation** (`match` / `find_prefix`): Tracks active NFA
states using precomputed epsilon closures and per-call `MatchContext` state.
## Headers
| Header | Purpose |
|--------|---------|
| `regex_engine.h` | Engine implementation (all phases, `RuneClass`, sentinels) |
| `unicode_data.h` | Unicode 15.1 General Category range tables + precomputed ASCII bitmaps |
| `utf8.h` | UTF-8 encoding/decoding, `rune_t` type definition |
`regex_engine.h` includes both `unicode_data.h` and `utf8.h`.
## Public API
```cpp
RegexEngine(const std::string_view& utf8_regexp);
RegexEngine(const std::string_view& utf8_regexp, SyntaxMode mode);
bool ok() const;
size_t num_captures() const;
SyntaxMode syntax_mode() const;
bool match(const std::string_view& utf8_str) const;
bool match(
const std::string_view& utf8_str,
MatchContext& ctx) const;
size_t find_prefix(
const std::string_view& utf8_str,
bool at_start = true) const;
size_t find_prefix(
const std::string_view& utf8_str,
MatchContext& ctx,
bool at_start = true) const;
size_t find_prefix(
const std::string_view& utf8_str,
std::vector<Capture>& captures,
bool at_start = true) const;
size_t find_prefix(
const std::string_view& utf8_str,
std::vector<Capture>& captures,
MatchContext& ctx,
bool at_start = true) const;
SearchResult search(
const std::string_view& utf8_str,
size_t start_pos = 0) const;
SearchResult search(
const std::string_view& utf8_str,
std::vector<Capture>& captures,
MatchContext& ctx,
size_t start_pos = 0) const;
```
- `SyntaxMode::Extended` (default) enables the full regex syntax.
`SyntaxMode::IregexpStrict` restricts to RFC 9485 iregexp.
- `ok()` returns true when the regex compiled successfully (no errors).
- `match()` checks full-string match for the given input.
- `find_prefix()` returns the longest matching prefix length, or `npos`.
- `search()` finds the first match anywhere in the string starting from a byte
offset. Returns a `SearchResult` with `match_start`, `match_len`, and
`found()`. The capture-aware overload fills group spans and reuses a
`MatchContext` across calls. Used by `TRegex::GlobalReplace`.
- Capture-aware `find_prefix()` fills group spans as byte offsets.
- `at_start` controls whether `^` can match in this invocation. This is used by
wrapper-level scanning logic (for example global replacement probe loops).
- `MatchContext` is the reusable per-caller/per-thread scratch state for
simulation and stats.
## Rune Constants
All constants live in namespace `trieste::regex` as `inline constexpr`.
### ASCII character constants
Named aliases for ASCII characters that have syntactic meaning in regexes:
| Constant | Value | Purpose |
|-------------|--------|--------------------------|
| `Backslash` | `'\\'` | Escape character |
| `LParen` | `'('` | Open group |
| `RParen` | `')'` | Close group |
| `Pipe` | `'|'` | Alternation syntax |
| `Question` | `'?'` | Zero-or-one syntax |
| `Asterisk` | `'*'` | Zero-or-more syntax |
| `Plus` | `'+'` | One-or-more syntax |
### Operator sentinel runes
Internal operator tokens placed in the postfix stream. Their values are in the
range `0xAFFF00–0xAFFFFF`, well outside Unicode, so they cannot collide with
literal runes (including escaped operators like `\*`):
| Sentinel | Value | Arity | Precedence |
|---------------|------------|--------|------------|
| `Catenation` | `0xAFFF00` | Binary | 2 (middle) |
| `Alternation` | `0xAFFF01` | Binary | 1 (lowest) |
| `ZeroOrOne` | `0xAFFF02` | Unary | 3 (highest, emitted immediately) |
| `ZeroOrMore` | `0xAFFF03` | Unary | 3 (highest, emitted immediately) |
| `OneOrMore` | `0xAFFF04` | Unary | 3 (highest, emitted immediately) |
| `Split` | `0xAFFF05` | — | NFA-only (epsilon state) |
| `WordBoundary`| `0xAFFF06` | Unary | Conditional epsilon (`\b`) |
| `LazyZeroOrOne` | `0xAFFF07` | Unary | Lazy quantifier `??` |
| `LazyZeroOrMore`| `0xAFFF08` | Unary | Lazy quantifier `*?` |
| `LazyOneOrMore` | `0xAFFF09` | Unary | Lazy quantifier `+?` |
| `StartAnchor` | `0xAFFF0A` | Unary | Conditional epsilon (`^`) |
| `EndAnchor` | `0xAFFF0B` | Unary | Conditional epsilon (`$`) |
| `Match` | `0xAFFFFF` | — | NFA-only (accept state) |
### Class-ref sentinels
A label in the range `[RuneClassBase, RuneClassMax]` (`0xBF0000–0xBFFFFF`)
is not a literal rune but an index into `RegexEngine::rune_classes_`. This
allows `StateDef` to remain unchanged — a class-ref is just a label value
that `step()` dispatches differently.
| Constant | Value | Purpose |
|-----------------|------------|---------|
| `RuneClassBase` | `0xBF0000` | Start of class-ref index space |
| `RuneClassMax` | `0xBFFFFF` | End of class-ref index space (64K classes) |
Helper functions: `is_class_ref(label)`, `class_ref_index(label)`.
### Resource limits (RFC 9485 §8)
| Constant | Value | Enforced in |
|------------------|----------|-------------|
| `MaxRepetition` | `1000` | Range quantifier parsing |
| `MaxPostfixSize` | `100000` | End of shunting-yard + range expansion |
| `MaxStates` | `100000` | Start of `postfix_to_nfa` |
| `MaxCaptures` | `64` | Capturing group indexing |
| `MaxGroupNesting`| `256` | Nested group depth during parsing |
| `MaxClosureCacheEntries` | `1000000` | `precompute_epsilon_closures` |
## Utility Functions
### `decode_rune(utf8_str, pos)`
```cpp
inline std::pair<rune_t, size_t>
decode_rune(const std::string_view& utf8_str, size_t pos);
```
Decodes one rune from `utf8_str` at byte offset `pos`. Returns
`{rune_value, bytes_consumed}`. **ASCII fast path**: if the byte at `pos` is
< 128, returns immediately without calling `utf8_to_rune`. Used by all
simulation loops — callers must ensure `pos < utf8_str.size()`.
## Key Data Structures
### `RuneClass`
A sorted, non-overlapping set of `[lo, hi]` inclusive `rune_t` intervals.
Used to represent dot (`.`), character classes (`[a-z]`), and Unicode
categories (`\p{L}`).
Fields:
- `ranges` — `std::vector<std::pair<rune_t, rune_t>>` of sorted intervals.
- `ascii_bitmap[2]` — `uint64_t[2]` bit-per-codepoint bitmap for runes
0–127. `ascii_bitmap[r >> 6] >> (r & 63) & 1` tests membership in O(1).
Rebuilt automatically by `normalize()`, `complement()`, and `dot()`.
Methods:
- `contains(rune_t)` — ASCII bitmap check for `r < 128`, else O(log k)
binary search on `ranges`.
- `add_range(lo, hi)` — push a range without normalizing. Call `finalize()`
when done.
- `merge(other)` — append another class's ranges without normalizing. Call
`finalize()` when done.
- `finalize()` — sort, merge overlapping ranges, and rebuild the ASCII
bitmap. Must be called after all `add_range`/`merge` calls are complete,
before the class is used for matching or `complement()`.
- `complement()` — complement over Unicode scalar values (0–0xD7FF,
0xE000–0x10FFFF), excluding surrogates. Requires finalized input.
- `dot()` — static factory returning all Unicode scalar values.
Private:
- `rebuild_ascii_bitmap()` — scans `ranges` and sets bits for runes 0–127.
`RuneClass` objects are stored in a `std::vector<RuneClass> rune_classes_`
member on `RegexEngine`. `make_class_ref(rc)` appends a class and returns
the corresponding class-ref sentinel for use in the postfix stream.
### `StateDef`
Each NFA state is a `StateDef`, stored contiguously in a pre-reserved
`std::vector<StateDef> owned_states_`. Referenced internally via raw pointer
alias `State = StateDef*`.
Fields:
- `label` — the rune this state matches, a class-ref sentinel (dispatched via
`RuneClass::contains()`), or `Split`/`Match` for control states.
- `next` — primary successor state.
- `next_alt` — secondary successor (used only by `Split` states).
- `closure_index` — dense state index for closure cache and dedup.
- `ascii_accept[2]` — `uint64_t[2]` per-state bitmap. Populated by
`finalize_states()`: class-ref states copy their RuneClass's
`ascii_bitmap`, literal states with label < 128 set the single
corresponding bit, all others remain zero. Enables `step()` to bypass
`is_class_ref`/`class_ref_index`/`contains` for ASCII runes.
- `trivial_closure` — `bool`, set by `precompute_epsilon_closures()`.
True when the state's epsilon closure is `{self}` for all 8 flag
combinations (always true for literal/class-ref/Match states). Enables
`add_state()` to skip the closure cache lookup.
Ownership/lifetime model:
- The constructor computes the postfix first, then reserves
`owned_states_` with capacity `2 * postfix.size() + 1` to guarantee no
reallocation during Thompson construction. This keeps raw `State`
pointers (including `Frag::dangling` targets) stable.
- `create_state()` appends a `StateDef` to `owned_states_` and returns a
raw pointer to it.
- All construction temporaries (the fragment stack, closure-computation
epoch vectors) are function-local — they exist only during the
constructor call and are destroyed automatically.
- `RegexEngine` is explicitly non-copyable and movable to keep raw-pointer
ownership semantics safe and obvious.
### `MatchContext`
`MatchContext` is public API with private internals (friend to `RegexEngine`).
It holds per-call mutable state:
- `noncapturing_current_states`, `noncapturing_next_states` — scratch vectors
for non-capturing simulation.
- `visited_states` — per-state epoch vector for dedup (used when > 128
states).
- `visited_bits_[2]` — `uint64_t[2]` bitset for dedup when ≤ 128 states
(`BitsetMaxStates`). Faster than epoch-based dedup for small NFAs.
- `use_bitset_` — chosen at `bind_engine()` time based on `state_count`.
- `capture_frames` — contiguous arena for capture slot storage.
- `capture_traversal_stack_` — `std::vector<std::pair<State, size_t>>`
reusable stack for iterative `add_state_capturing`.
- `epoch_counter` — monotonic counter for epoch-based dedup.
- Optional `MatchStats` (gated by `TRIESTE_REGEX_ENGINE_ENABLE_STATS`).
Reuse model:
- Reuse a context across calls in one caller/thread.
- Do not use the same context concurrently across threads.
- Each match call binds the context to the engine, allowing safe context reuse
across different compiled engine instances.
### `MatchStats`
Optional counters (gated by `TRIESTE_REGEX_ENGINE_ENABLE_STATS`):
- `rune_steps` — total input positions processed.
- `active_states_total` — cumulative active states across steps.
- `max_active_states` — peak active state count.
- `class_ref_checks` — class-ref dispatch count (non-ASCII path only).
- `literal_checks` — literal label comparisons (non-ASCII path only).
### `Capture` and `Thread`
- `Capture` stores `[start, end)` byte offsets and a `matched()` helper.
- Capture-aware simulation uses `Thread { state, caps_frame }` where
`caps_frame` indexes a contiguous capture-frame arena.
- Capture slot storage is held in MatchContext's frame arena with fixed frame
width `2 * num_captures_`.
- Capture open/close epsilon transitions clone frame contents, update one slot,
and continue traversal so sibling branches keep independent capture state.
### `Frag`
A fragment of an NFA under construction:
- `start` — the entry state of the fragment.
- `dangling` — a `std::list<State*>` of raw pointers into the `next` or
`next_alt` members of states within the fragment. `patch()` sets them all
to point to a given target state.
### `ClosureSpan`
A lightweight view into the flat closure cache:
- `data` — pointer into `closure_cache_flat_`.
- `size` — number of states in this closure.
Returned by `epsilon_closure_cached()`.
## Phase Details
### Phase 1: Shunting-yard (`regexp_to_postfix_runes`)
Converts infix regex syntax to postfix tokens with:
- implicit concatenation (`Catenation`)
- alternation (`Alternation`)
- quantifiers (`?`, `*`, `+`, `{n}`, `{n,m}`, `{n,}`)
- lazy quantifiers (`??`, `*?`, `+?`)
- assertions (`\b`, `^`, `$`)
- groups (capturing and non-capturing `(?:...)`)
- class refs (dot, bracket classes, unicode categories, shorthands)
#### Escape Handling
The parser accepts these escapes:
| Pattern | Result |
|---------|--------|
| `\n`, `\r`, `\t` | Literal runes 0x0A, 0x0D, 0x09 |
| `\p{Xx}` | Unicode General Category lookup via `unicode_data.h`; emits class-ref |
| `\P{Xx}` | Same but complemented |
| `\d`, `\D`, `\s`, `\S`, `\w`, `\W` | Shorthand class refs |
| `\xNN` | Byte-valued hex escape |
| `\b` | Word-boundary assertion |
| `\(`, `\)`, `\*`, `\+`, `\-`, `\.`, `\?`, `\[`, `\\`, `\]`, `\^`, `\$`, `\{`, `\|`, `\}`, `\/`, `\,` | Literal rune |
| Any other escape | **malformed** |
| Trailing `\` | **malformed** |
Inside bracket classes, unicode categories and shorthand classes are merged
into the bracket class.
#### Range Quantifiers
Range quantifiers (`{n}`, `{n,m}`, `{n,}`) are handled by **postfix
expansion** — the atom's postfix tokens are extracted and replicated. This
keeps `postfix_to_nfa` unchanged. Tracking variables:
| Variable | Purpose |
|----------|---------|
| `atom_start` | Index in postfix where the current atom begins |
| `group_start_stack` | Saves `(postfix_index, need_concat)` on `(` |
| `need_concat_before_atom` | Whether a Cat was pushed before this atom |
| `atom_is_quantified` | Detects nested quantifiers (→ malformed) |
Expansion formula (postfix notation, Cat is the `Catenation` operator):
| Pattern | Postfix expansion |
|---------|-------------------|
| `a{n}` | `a × n` chained with Cat |
| `a{n,m}` | `a × n` + `(a ZeroOrOne Cat) × (m−n)` |
| `a{n,}` | `a × n` + `a ZeroOrMore Cat` |
| `a{0}` | Atom removed, preceding Cat undone |
Nested quantifiers (e.g. `a*{2}`) are rejected as malformed. Expansion size
is checked upfront against `MaxPostfixSize`.
#### Rejected Syntax
The following patterns are rejected as malformed:
- Bare `]` or `}` outside their context.
- Empty character class `[]` or `[^]`.
- Inverted ranges `[z-a]`.
- Range quantifiers exceeding `MaxRepetition`.
- Unterminated constructs (`[abc`, `a{`, `a\`).
- Unknown/unsupported escapes.
### Phase 2: Thompson's construction (`postfix_to_nfa`)
Walks the postfix runes left-to-right, maintaining a stack of `Frag` objects:
| Postfix token | Action |
|---------------|--------|
| Literal rune or class-ref | Create a new state, push fragment |
| `Catenation` | Pop two, patch left's dangling to right's start |
| `Alternation` | Pop two, create Split pointing to both starts |
| `ZeroOrOne` | Pop one, create Split with one branch to fragment |
| `ZeroOrMore` | Pop one, create Split looping back |
| `OneOrMore` | Pop one, create Split looping back, entry is original start |
The postfix size is checked against `MaxStates` before construction begins.
Stack underflow sets `error_code_` and returns the accept state as a safe
sentinel.
### Phase 3: Finalization
Two functions run after Thompson construction:
**`precompute_epsilon_closures()`**: For every state and for all 8 flag
combinations (boundary × at_start × at_end), computes the full epsilon
closure and stores it in a flat array (`closure_cache_flat_`) indexed via
an offset table (`closure_cache_offsets_`). Also sets `trivial_closure =
true` on any state whose closure is `{self}` for all 8 combinations. Total
closure entries are bounded by `MaxClosureCacheEntries`. The epoch vector
and counter used for cycle detection are local to this function.
**`finalize_states()`**: Populates `ascii_accept[2]` on each state:
class-ref states copy the RuneClass's `ascii_bitmap`, literal states with
label < 128 set their single bit, all others stay zero. Since
`owned_states_` is already contiguous (pre-reserved `vector<StateDef>`),
no copy or pointer remapping is needed.
### Phase 4: Simulation (`match`, `find_prefix`)
Uses the standard Thompson NFA simulation with several fast paths:
1. `start_list` seeds the initial active set via epsilon closure.
2. For each input rune, `step` builds the next state set. The loop has two
branches:
- **ASCII fast path** (`rune < 128`): for each active state, test the
per-state `ascii_accept` bitmap. This single bit-test replaces the
entire class-ref dispatch chain (`is_class_ref` → `class_ref_index` →
`rune_classes_[]` → `RuneClass::contains()`).
- **Non-ASCII fallback**: for each active state, dispatch via
`is_class_ref(label)` + `contains(rune)` or literal equality.
3. Conditional epsilon states use booleans threaded through simulation:
- `boundary_match` for `\b`
- `at_start` for `^`
- `at_end` for `$`
4. `add_state()` has a **trivial-closure shortcut**: if `state->trivial_closure`
is true, the state is added directly with dedup, bypassing the closure
cache lookup entirely. Otherwise the flat closure cache is consulted.
5. Input decoding uses `decode_rune()` which has an ASCII fast path avoiding
the full `utf8_to_rune` call for bytes < 128.
**Conditional detection**: `detect_conditional_states()` scans the NFA for
`WordBoundary`, `StartAnchor`, or `EndAnchor`. When absent, simulation
skips `is_word_char()` calls and passes constant `false` for all
boundary/anchor parameters, providing separate code paths for conditional
and non-conditional patterns.
**Capture-aware simulation** (`add_state_capturing`): Iterative (stack-based)
epsilon traversal using `capture_traversal_stack_` in MatchContext. Pushes
`next_alt` before `next` so `next` is popped first (greedy DFS ordering).
Capture open/close transitions clone the capture frame and update one slot
before continuing. The stack is reused across calls via MatchContext.
**Epoch-based deduplication**: For NFAs with > 128 states, MatchContext
tracks per-state epochs in a visited-state vector. A per-context epoch
counter increments before each step, and `add_state` skips states already
marked in the current epoch. For NFAs with ≤ 128 states, a 128-bit bitset
(`visited_bits_[2]`) is used instead for faster clear and test.
## Error Handling
The engine uses an `error_code_` field (type `ErrorCode`) rather than
exceptions or error tokens:
- Set during `regexp_to_postfix_runes` (invalid syntax, invalid escapes,
resource limits) or `postfix_to_nfa` (stack underflow, state limit).
- Once set, `match()` returns false immediately.
- Callers should check `ok()` after construction, or inspect `error_code()`
and `error()` for a human-readable message.
## Performance Notes
### Compile+match advantage
Trieste's NFA compile is much cheaper than RE2's DFA. On benchmarks, the
compile+match ratio (`cm_ratio`) is ~0.31x (Trieste ~3x faster than RE2
overall). This makes the engine well-suited for single-use patterns.
### Match-only gap
The match-only ratio (`m_ratio`) is ~2.6x. This is structural: RE2's DFA
does O(1) work per rune after warmup, while NFA simulation is O(active_states)
per rune. The gap is largest on patterns with many steps and high active
state counts.
### Key optimizations
- **Per-state ASCII bitmap**: `ascii_accept[2]` on StateDef unifies
class-ref and literal dispatch into a single bit-test for runes < 128.
Eliminates the `is_class_ref` → `class_ref_index` → `rune_classes_[]` →
`contains()` chain for the common case.
- **RuneClass ASCII bitmap**: `ascii_bitmap[2]` on RuneClass provides O(1)
membership test for runes < 128, avoiding binary search on `ranges`.
Unicode category bitmaps are precomputed at compile time in
`unicode_data.h`.
- **Trivial-closure shortcut**: States whose epsilon closure is always
`{self}` (literal, class-ref, Match states) skip the closure cache
lookup in `add_state()`.
- **ASCII `decode_rune` bypass**: `decode_rune()` returns immediately for
bytes < 128 without calling `utf8_to_rune`.
- **Bitset dedup**: NFAs with ≤ 128 states use a 128-bit bitset for
visited-state tracking instead of epoch-based vector dedup.
- **Flat closure cache**: `closure_cache_flat_` + `closure_cache_offsets_`
store precomputed epsilon closures in contiguous memory, indexed by
`(closure_index << 3) | flags`.
- **Pre-reserved contiguous storage**: `owned_states_` is a
`vector<StateDef>` reserved upfront based on postfix size, so all states
are contiguous in memory for cache-friendly simulation without a separate
compaction pass.
- **Iterative capturing traversal**: `add_state_capturing` uses an explicit
stack (`capture_traversal_stack_` in MatchContext) instead of recursion,
eliminating per-epsilon function-call overhead.
- **Capture-frame arena**: capture-aware simulation avoids per-thread vector
copies by storing capture slots in a contiguous arena.
- **Non-capturing scratch reuse**: `match()` and non-capturing
`find_prefix()` reuse MatchContext vectors to avoid per-call allocations.
- **Optional stats**: `TRIESTE_REGEX_ENGINE_ENABLE_STATS` gates counters
stored in MatchContext and exposed via `MatchContext::stats()`.
## `unicode_data.h` Extensions
Beyond the auto-generated Unicode 15.1 General Category range tables,
`unicode_data.h` contains compile-time ASCII bitmap infrastructure:
- `AsciiBitmap` — struct with `uint64_t words[2]`.
- `compute_ascii_bitmap<N>(ranges)` — `constexpr` template that scans a
range table and produces an `AsciiBitmap` for runes 0–127.
- 36 `inline constexpr AsciiBitmap` constants (one per Unicode General
Category), precomputed at compile time.
- `CategoryInfo` struct extended with `AsciiBitmap ascii_bitmap` field.
- `find_category()` returns precomputed bitmaps alongside range data.
These precomputed bitmaps are used when constructing a `RuneClass` from a
Unicode category, avoiding runtime bitmap computation.
## Behavior Notes
- Empty pattern compiles to an accept-only NFA.
- `find_prefix()` can produce zero-length matches.
- Anchors are implemented in-engine (`^`, `$`) and do not require wrapper-only
emulation.
- The engine operates on UTF-8 codepoint decoding, but capture offsets are byte
offsets (matching existing parser/rewrite expectations).
## Additional Syntax Coverage
Beyond the core iregexp-compatible pieces, parser support includes:
- **Capturing and non-capturing groups**: `( ... )`, `(?: ... )`.
- **Capture operators in postfix/NFA**: `CaptureGroup + i` wraps fragments with
epsilon `CaptureOpen + i` and `CaptureClose + i` states.
- **Lazy quantifiers**: `??`, `*?`, `+?` (implemented by swapped `Split`
branch preference in Thompson construction).
- **POSIX classes inside brackets**: `[:alpha:]`, `[:digit:]`, `[:alnum:]`,
`[:blank:]`, `[:space:]`, `[:xdigit:]`, `[:upper:]`, `[:lower:]`,
`[:print:]`, `[:graph:]`, `[:cntrl:]`, `[:punct:]`, `[:ascii:]`.
## Conventions
- **Namespace**: `trieste::regex` for the engine; `trieste::unicode` for
category data.
- **`inline constexpr`** for namespace-scope rune constants and resource
limits.
- **`StateDef*`** aliased as `State`; owned by `owned_states_` (a
pre-reserved `vector<StateDef>`).
- **Thread-safety model**: Engine matching methods are `const` and engine state
is read-only after construction. Thread safety depends on context usage:
sharing one `MatchContext` across threads is unsafe; using separate contexts
per thread is safe.
- **Header-only**: The engine is `regex_engine.h`; category data is in
`unicode_data.h` (auto-generated from Unicode 15.1 `UnicodeData.txt`).
## Modification Guidelines
When extending the regex engine:
1. **New set-based matchers** (e.g. new Unicode properties): Create a
`RuneClass`, store it via `make_class_ref()`, and emit the class-ref in
the postfix stream. No changes to `StateDef` or `postfix_to_nfa` needed.
The ASCII bitmap will be computed automatically by `normalize()`.
2. **New operators**: Assign a sentinel rune in the `0xAFFF00` range and update
parsing plus NFA construction.
3. **New conditional epsilon states**: Update both `add_state` and
`add_state_capturing`, and thread required context booleans through
`start_list`/`step`/`find_prefix` call paths. Update the 3-bit flag
encoding in `closure_flags()` and increase the slot multiplier from 8.
4. **Testing**: Add test cases to `test/regex_engine_test.cc`. Test positive
matches, negative matches, and malformed pattern rejection for any new
error conditions. For assertions, include both full match and prefix/probe
scenarios.
5. **Benchmarking**: Run `test/regex_engine_benchmark.cc` to verify
performance impact. Compare `cm_ratio` and `m_ratio` against baseline.
To produce a chart and markdown report from multiple benchmark passes, run:
```
.venv/bin/python .github/skills/regex-engine/benchmark_chart.py
```
This script runs the benchmark N times (default 5, with warmup), computes
medians, and outputs a PNG chart + markdown report. Pass `--input <json>`
to re-render from previously saved data without re-running benchmarks.
6. **Keep phases separate**: Parsing (shunting-yard), construction (Thompson),
compaction (closures + arena), and simulation are cleanly separated.
Changes should respect these boundaries.
7. **Unicode data**: If upgrading the Unicode version, regenerate
`unicode_data.h` from the new `UnicodeData.txt`. Remember to also update
the `compute_ascii_bitmap` constants and `CategoryInfo` entries.More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

