Skip to content

fix(regex): don't recompile a cached pattern just to re-validate it - #5777

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix-regexp-recompile-on-cache-hit
Jun 29, 2026
Merged

fix(regex): don't recompile a cached pattern just to re-validate it#5777
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix-regexp-recompile-on-cache-hit

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Problem

js_regexp_new validates every pattern by compiling it with both the regex and fancy-regex engines (to decide whether to throw SyntaxError) — and does so before consulting REGEX_CACHE. So a regex literal evaluated in a hot loop recompiled its automaton on every single call, even though get_or_compile_regex had already cached the compiled form on the first call.

This is pathological for large patterns. string-width's emojiRegex() returns a fresh /…/g literal (12807 chars — a huge alternation) on every text measurement, and ink's flexbox calculateLayout measures every cell. The result: rendering the splash sat at ~99% CPU for minutes, rebuilding the same automaton thousands of times.

Backtrace of the spin:

UV8/TS1.calculateLayout → recursive yr6 → js_array_map
  → js_regexp_new → build_std_regex → regex_syntax Hir::alternation

Fix

Skip the expensive validation compile when (pattern, flags) is already in REGEX_CACHE. A cached pattern is by definition compilable, so the "both engines fail → throw" branch can never fire for it.

  • The cheap JS-syntax checks (has_invalid_repeated_quantifier, the /u-flag legacy-escape checks) still run on every call.
  • get_or_compile_regex still returns the cached Arc<Regex>, and each RegExp object still gets its own fresh header (its own lastIndex).
  • Uncached patterns are validated exactly as before, so invalid patterns still throw.

Net effect: 30,000 fresh-literal compiles of a large pattern drop from minutes to ~25ms (cache hit).

Test

regexp_cache_repeated_compile.rs asserts a repeatedly-compiled literal keeps matching correctly, two regexes from the same pattern keep independent lastIndex, flags stay part of the cache key, and invalid patterns still throw on every call — run under a wall-clock timeout so a regression to per-call recompilation fails fast.

Summary by CodeRabbit

  • Performance Improvements

    • Improved regular expression handling by reusing cached compiled patterns, reducing repeated validation work on cache hits.
    • Repeated use of the same regex pattern now runs faster while preserving correct matching behavior.
  • Bug Fixes

    • Kept lastIndex behavior independent for separate regex instances created from the same pattern.
    • Ensured invalid regex patterns still raise errors consistently and are not cached.
    • Preserved correct cache separation for different regex flags.

`js_regexp_new` validated every pattern by compiling it with BOTH the `regex`
and `fancy-regex` engines (to decide whether to throw `SyntaxError`) — and did
so BEFORE consulting `REGEX_CACHE`. So a regex literal evaluated in a hot loop
recompiled its automaton on every single call even though `get_or_compile_regex`
had already cached the compiled form from the first call.

This is pathological for large patterns. string-width's `emojiRegex()` returns
a fresh `/…/g` literal (12807 chars, a huge alternation) on every text
measurement; ink's flexbox `calculateLayout` measures every cell, so the splash
render sat at ~99% CPU for minutes rebuilding the same automaton thousands of
times.

Fix: skip the expensive validation compile when `(pattern, flags)` is already
in `REGEX_CACHE`. A cached pattern is by definition compilable, so the
"both engines fail → throw" branch can never fire for it. The cheap JS-syntax
checks (`has_invalid_repeated_quantifier`, the `/u`-flag legacy-escape checks)
still run on every call, and `get_or_compile_regex` below returns the cached
`Arc<Regex>` as before — each `RegExp` object still gets its own fresh header
(and thus its own `lastIndex`). Uncached patterns are validated exactly as
before.

Adds an integration test asserting a repeatedly-compiled literal keeps matching
correctly, that two regexes from the same pattern keep independent `lastIndex`,
that flags stay part of the cache key, and that invalid patterns still throw on
every call. The test runs under a wall-clock timeout so a regression to per-call
recompilation fails fast.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

js_regexp_new now checks REGEX_CACHE for a (pattern, flags) key before running the dual-engine syntax validation step, skipping recompilation on cache hits. A new integration test validates correctness across repeated regex use, flag-sensitive cache keying, independent lastIndex, and continued SyntaxError for invalid patterns.

Regex cache validation skip

Layer / File(s) Summary
Cache hit bypass in js_regexp_new
crates/perry-runtime/src/regex.rs
Adds a REGEX_CACHE lookup at the top of js_regexp_new; only runs dual-engine SyntaxError validation on cache misses, skipping recompilation on hits.
Regression integration test
crates/perry/tests/regexp_cache_repeated_compile.rs
Adds a test harness and core integration test that compiles a generated TypeScript program and asserts match correctness on cache hits, flag-sensitive cache keying, independent lastIndex, 30s timeout, and continued SyntaxError for uncached invalid patterns.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PerryTS/perry#5749: Modifies the same js_regexp_new validation path in crates/perry-runtime/src/regex.rs, adding Annex B.1.4 forbidden-pattern checks that overlap with the constructor logic changed here.

Poem

🐇 Hop, hop, the cache is warm,
No need to recompile the form!
Pattern and flags already known—
Skip the engines, fully grown.
The rabbit checks the shelf and grins:
"Already there!" — and swiftly wins. 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main change to avoid recompiling cached regex patterns.
Description check ✅ Passed It covers the problem, fix, and test plan, but it doesn't follow the template's Summary/Changes/Related issue sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry/tests/regexp_cache_repeated_compile.rs (1)

46-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the timeout regression signal.

This timeout only becomes meaningful if the repeated construction is expensive enough to distinguish cache hits from recompilation. With /(\d+)-(\w+)/ over 1000 iterations, the test mostly proves semantic correctness; a recompile regression could still finish well under 30s. Consider swapping in a materially larger pattern (or many more iterations) so the timeout actually exercises the performance regression this PR is fixing.

Also applies to: 87-95

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/tests/regexp_cache_repeated_compile.rs` around lines 46 - 55,
Strengthen the timeout regression test so it actually detects repeated
recompilation slowdowns instead of only verifying correctness. Update the
repeated-compile benchmark in regexp_cache_repeated_compile.rs, specifically the
test that spawns the compiled binary and measures elapsed time, by using a
materially larger regex pattern and/or significantly more iterations than the
current 1000-loop case. Keep the same timeout-based structure, but make the
workload heavy enough that cache hits and recompilation diverge clearly, so the
wall-clock timeout meaningfully fails on regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry/tests/regexp_cache_repeated_compile.rs`:
- Around line 46-55: Strengthen the timeout regression test so it actually
detects repeated recompilation slowdowns instead of only verifying correctness.
Update the repeated-compile benchmark in regexp_cache_repeated_compile.rs,
specifically the test that spawns the compiled binary and measures elapsed time,
by using a materially larger regex pattern and/or significantly more iterations
than the current 1000-loop case. Keep the same timeout-based structure, but make
the workload heavy enough that cache hits and recompilation diverge clearly, so
the wall-clock timeout meaningfully fails on regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 30487329-e08f-4171-aaac-eb7318b12699

📥 Commits

Reviewing files that changed from the base of the PR and between c002052 and 2800594.

📒 Files selected for processing (2)
  • crates/perry-runtime/src/regex.rs
  • crates/perry/tests/regexp_cache_repeated_compile.rs

@proggeramlug
proggeramlug merged commit aff1290 into PerryTS:main Jun 29, 2026
15 checks passed
proggeramlug added a commit that referenced this pull request Jun 30, 2026
…5808)

`js_regexp_new` ran three JS-syntax validation checks
(`has_invalid_repeated_quantifier`, `has_unicode_forbidden_legacy_escape`,
`has_unicode_forbidden_pattern`) on EVERY call, before the `if !in_cache`
gate that already skipped the expensive both-engines recompile (#5777).

These checks are not actually cheap: `has_invalid_repeated_quantifier`
does a `pattern.chars().collect::<Vec<char>>()` (a ~51 KB allocation for a
12,807-char pattern) plus an O(n) scan. The common `string-width` /
`emoji-regex` npm packages construct a fresh ~12,807-char `/…/g` literal on
every measurement, and a layout pass can call them thousands of times, so
this re-validation — not the already-cached compile — became the top hot
frame in profiles.

Move all three checks inside the existing `if !in_cache { ... }` block (with
`in_cache` computed first), so the whole validation block is skipped on a
REGEX_CACHE hit. This is safe: regex validity is a pure function of
(pattern, flags); an invalid pattern throws before `get_or_compile_regex`
can cache it; and both writers of REGEX_CACHE (`js_regexp_new` and
`regex/compile.rs`) run these exact checks before populating the cache, so
any cache hit is provably valid. Invalid patterns are never cached, so they
remain cache misses and still throw SyntaxError on every call.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Aug 30, 2026
…-code's 2,378 literals 232 → 50 ms

A symbolized `perf` profile of the claude-code bundle running `--help` — a
command that prints help text and exits — put **14.3% of all retired
instructions inside regex COMPILATION**: `ClassUnicodeRange::case_fold_simple`
3.53%, `thompson::compiler::Compiler::c` 2.16%, `determinize::next` 1.94%,
`add_nfa_states` 0.84%, plus the remainder across `regex_syntax` /
`regex_automata`. The whole of cc's compiled JavaScript is 0.11% of the same
profile.

## When compilation happened

At construction, for every regex the program HAS, not every regex it USES.

`js_regexp_new` — what both a `/…/` literal (`Expr::RegExp` in
`codegen/expr/logical_collections.rs`) and `new RegExp(…)` lower to — answered
"is this pattern a SyntaxError?" by BUILDING the pattern.
`compile_and_cache_regex_checked` is a full `regex::Regex::new`: parse, HIR
translate (Unicode class expansion and, under `i`, `case_fold_simple`),
Thompson NFA, meta strategy selection. The result was installed on the header
and cached thread-locally under `(pattern, canonical_flags)` in `REGEX_CACHE` /
`FANCY_CACHE` / `REPEAT_MATCHER_CACHE` (512 entries, cleared wholesale on
overflow). A regex literal is evaluated when its module initialises, so a
bundle pays for every literal it contains.

## The proof

A fixture of N regex literals of realistic shape (Unicode ranges, alternations,
`i`/`u` flags) where exactly ONE is ever executed, and a second fixture built
from **every distinct regex literal in the claude-code bundle** (2,378 of them,
extracted from `cli_2.1.112.js`), again matching with exactly one. Construction
time is the program's own `Date.now()` delta; min of 9 runs.

| literals constructed, 1 used | before | after | node |
|---|---|---|---|
| 50 | 19 ms | 2 ms | 0 ms |
| 200 | 73 ms | 7 ms | 1 ms |
| 400 | 145 ms | 15 ms | 3 ms |
| **2,378 (real claude-code literals)** | **232 ms** | **50 ms** | 5 ms |

Perfectly linear in the count before the change — 362 µs per literal — which is
the signature of "every literal compiles". Whole-process wall clock for the
claude-code corpus: 247.5 → 59.9 ms.

## What changed

Only the *program build* moves; everything observable at construction stays at
construction. New `regex/lazy.rs`:

* **`js_regexp_new` no longer builds.** `regex_ptr` (with `fancy_ptr` /
  `repeat_matcher_ptr`) is left null — the "not built yet" state — and
  `ensure_regex_compiled` installs all three, from the same caches, on the
  first operation that needs a matcher. Every `&*(*re).regex_ptr` in the tree
  now goes through `header_std_regex`, and `lookup_fancy_regex` /
  `lookup_repeat_matcher` build first, so a null there cannot be confused with
  "this pattern has no fallback". Publishing `regex_ptr` last keeps it a sound
  built/not-built flag.

* **Validation stays eager, and gets cheap.** A syntactically invalid pattern
  must still throw `SyntaxError` from the same point in the program, so
  `js_regexp_new` still validates — but with the parser instead of the builder.
  `regex_syntax`'s AST parse is pure grammar (unbalanced groups, `a{2,1}`,
  `[z-a]`, dangling `)` all fail there); its HIR translate pass is where the
  Unicode class expansion and case folding live. The only translate-only
  diagnostic reachable from the strings perry produces is an unknown Unicode
  property name, so `std_engine_syntax_ok` AST-parses everything and pays for
  the full translate only when the translated pattern mentions `\p`/`\P` —
  0.7% of the claude-code literals (16 of 2,378).

  A parser rejection is not a verdict: every lookbehind/backreference pattern
  is rejected by the linear engine too, so that case falls through to the
  UNCHANGED both-engines path, which still owns the `SyntaxError` decision and
  still populates the caches for the fancy fallback.

* **`VALIDATED_PATTERNS`** replaces the `REGEX_CACHE`-hit gate on the whole
  validation block. Validity is a pure function of `(pattern, flags)`; PerryTS#5777
  keyed that skip off a cache hit, which worked only because construction also
  compiled. Same 512-entry cap and clear-on-overflow policy as the program
  caches.

* `ensure_regex_compiled` is `#[inline]` with the build in a `#[cold]` callee:
  it runs up to three times per match operation, and `is_valid_regex_ptr`
  reaches `try_read_gc_header`/heap-space classification (1.55% of the same
  profile), far too expensive to repeat once the program exists. The hot path
  is two loads.

Not lazy, deliberately: `RegExp.prototype.compile` (Annex B, rare) still builds
eagerly, and its own validation is untouched.

## Semantics

`test-files/test_gap_9163_lazy_regex_semantics.ts` is byte-identical to node
and covers the five things the deferral could break: `SyntaxError` still raised
at construction (including for a regex that is constructed and never matched
with); `.source`/`.flags`/the flag getters readable before any match and
unchanged after one; `/g` and `/y` `lastIndex` statefulness across the build
being installed mid-life; identity (a fresh object per evaluation, independent
`lastIndex` and expandos — the RegExp analogue of PerryTS#9128's closure-literal
singleton bug); and the fancy-regex + RepeatMatcher fallbacks being installed
by the deferred build too. `RegExp.prototype.compile` on a header whose program
was never built (it releases a null old pointer) is covered as well.

`tests::syntax_check_agrees_with_full_build` pins the cheap check against
`build_std_regex` on a committed corpus, and takes a `PERRY_REGEX_CORPUS` file
for the wide sweep it was developed against: every distinct regex literal in
the claude-code, pi and kimi bundles (3,402) plus 6,297 mutations of the
claude-code set (truncations, single-character deletions, an injected `{2,1}`)
to load the reject direction — **9,899 patterns, zero disagreements**, in both
directions, through perry's real `js_regex_to_rust` translation. The
claude-code corpus fixture also reports `rejected=3` identically before and
after: the three ~10-15 KB `/u` identifier classes perry already refused are
still refused, at the same point.

One corner does move, in node's direction: a pattern the parser accepts but the
NFA build rejects (i.e. one that blows the 64 MiB size budget) used to be a
construction-time `SyntaxError` and is now a silent never-match at first use.
Node raises no error for such a pattern either. Zero occurrences across the
9,899-pattern sweep.

## Validation

* `cargo test -p perry-runtime --lib -- --test-threads=1`: 2846 passed,
  0 failed, 4 ignored.
* `test-files/test_gap_9163_lazy_regex_semantics.ts` byte-identical to
  `node --experimental-strip-types`.
* `cargo fmt --all -- --check` clean; `check_file_size.sh`,
  `check_thread_locals.py`, `check_test_registration.py`,
  `workspace_architecture.py --check` all OK.
* `cargo check -p perry-runtime --no-default-features` (regex engine gated off)
  clean.
* Match-throughput control fixture (200k `test`, 200k `exec`, 100k
  `replace`, 100k `search` on a built regex): 220 → 198 ms min. The per-op null
  check costs nothing measurable.
* `.text` of a compiled fixture binary 8,530,132 → 8,536,468 bytes (+6,336,
  +0.07%). Codegen is unchanged; the growth is runtime only.

Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
proggeramlug added a commit that referenced this pull request Aug 30, 2026
…erals 232ms → 50ms at startup (#9178)

* perf(runtime): regex literals stop compiling at construction — claude-code's 2,378 literals 232 → 50 ms

A symbolized `perf` profile of the claude-code bundle running `--help` — a
command that prints help text and exits — put **14.3% of all retired
instructions inside regex COMPILATION**: `ClassUnicodeRange::case_fold_simple`
3.53%, `thompson::compiler::Compiler::c` 2.16%, `determinize::next` 1.94%,
`add_nfa_states` 0.84%, plus the remainder across `regex_syntax` /
`regex_automata`. The whole of cc's compiled JavaScript is 0.11% of the same
profile.

## When compilation happened

At construction, for every regex the program HAS, not every regex it USES.

`js_regexp_new` — what both a `/…/` literal (`Expr::RegExp` in
`codegen/expr/logical_collections.rs`) and `new RegExp(…)` lower to — answered
"is this pattern a SyntaxError?" by BUILDING the pattern.
`compile_and_cache_regex_checked` is a full `regex::Regex::new`: parse, HIR
translate (Unicode class expansion and, under `i`, `case_fold_simple`),
Thompson NFA, meta strategy selection. The result was installed on the header
and cached thread-locally under `(pattern, canonical_flags)` in `REGEX_CACHE` /
`FANCY_CACHE` / `REPEAT_MATCHER_CACHE` (512 entries, cleared wholesale on
overflow). A regex literal is evaluated when its module initialises, so a
bundle pays for every literal it contains.

## The proof

A fixture of N regex literals of realistic shape (Unicode ranges, alternations,
`i`/`u` flags) where exactly ONE is ever executed, and a second fixture built
from **every distinct regex literal in the claude-code bundle** (2,378 of them,
extracted from `cli_2.1.112.js`), again matching with exactly one. Construction
time is the program's own `Date.now()` delta; min of 9 runs.

| literals constructed, 1 used | before | after | node |
|---|---|---|---|
| 50 | 19 ms | 2 ms | 0 ms |
| 200 | 73 ms | 7 ms | 1 ms |
| 400 | 145 ms | 15 ms | 3 ms |
| **2,378 (real claude-code literals)** | **232 ms** | **50 ms** | 5 ms |

Perfectly linear in the count before the change — 362 µs per literal — which is
the signature of "every literal compiles". Whole-process wall clock for the
claude-code corpus: 247.5 → 59.9 ms.

## What changed

Only the *program build* moves; everything observable at construction stays at
construction. New `regex/lazy.rs`:

* **`js_regexp_new` no longer builds.** `regex_ptr` (with `fancy_ptr` /
  `repeat_matcher_ptr`) is left null — the "not built yet" state — and
  `ensure_regex_compiled` installs all three, from the same caches, on the
  first operation that needs a matcher. Every `&*(*re).regex_ptr` in the tree
  now goes through `header_std_regex`, and `lookup_fancy_regex` /
  `lookup_repeat_matcher` build first, so a null there cannot be confused with
  "this pattern has no fallback". Publishing `regex_ptr` last keeps it a sound
  built/not-built flag.

* **Validation stays eager, and gets cheap.** A syntactically invalid pattern
  must still throw `SyntaxError` from the same point in the program, so
  `js_regexp_new` still validates — but with the parser instead of the builder.
  `regex_syntax`'s AST parse is pure grammar (unbalanced groups, `a{2,1}`,
  `[z-a]`, dangling `)` all fail there); its HIR translate pass is where the
  Unicode class expansion and case folding live. The only translate-only
  diagnostic reachable from the strings perry produces is an unknown Unicode
  property name, so `std_engine_syntax_ok` AST-parses everything and pays for
  the full translate only when the translated pattern mentions `\p`/`\P` —
  0.7% of the claude-code literals (16 of 2,378).

  A parser rejection is not a verdict: every lookbehind/backreference pattern
  is rejected by the linear engine too, so that case falls through to the
  UNCHANGED both-engines path, which still owns the `SyntaxError` decision and
  still populates the caches for the fancy fallback.

* **`VALIDATED_PATTERNS`** replaces the `REGEX_CACHE`-hit gate on the whole
  validation block. Validity is a pure function of `(pattern, flags)`; #5777
  keyed that skip off a cache hit, which worked only because construction also
  compiled. Same 512-entry cap and clear-on-overflow policy as the program
  caches.

* `ensure_regex_compiled` is `#[inline]` with the build in a `#[cold]` callee:
  it runs up to three times per match operation, and `is_valid_regex_ptr`
  reaches `try_read_gc_header`/heap-space classification (1.55% of the same
  profile), far too expensive to repeat once the program exists. The hot path
  is two loads.

Not lazy, deliberately: `RegExp.prototype.compile` (Annex B, rare) still builds
eagerly, and its own validation is untouched.

## Semantics

`test-files/test_gap_9163_lazy_regex_semantics.ts` is byte-identical to node
and covers the five things the deferral could break: `SyntaxError` still raised
at construction (including for a regex that is constructed and never matched
with); `.source`/`.flags`/the flag getters readable before any match and
unchanged after one; `/g` and `/y` `lastIndex` statefulness across the build
being installed mid-life; identity (a fresh object per evaluation, independent
`lastIndex` and expandos — the RegExp analogue of #9128's closure-literal
singleton bug); and the fancy-regex + RepeatMatcher fallbacks being installed
by the deferred build too. `RegExp.prototype.compile` on a header whose program
was never built (it releases a null old pointer) is covered as well.

`tests::syntax_check_agrees_with_full_build` pins the cheap check against
`build_std_regex` on a committed corpus, and takes a `PERRY_REGEX_CORPUS` file
for the wide sweep it was developed against: every distinct regex literal in
the claude-code, pi and kimi bundles (3,402) plus 6,297 mutations of the
claude-code set (truncations, single-character deletions, an injected `{2,1}`)
to load the reject direction — **9,899 patterns, zero disagreements**, in both
directions, through perry's real `js_regex_to_rust` translation. The
claude-code corpus fixture also reports `rejected=3` identically before and
after: the three ~10-15 KB `/u` identifier classes perry already refused are
still refused, at the same point.

One corner does move, in node's direction: a pattern the parser accepts but the
NFA build rejects (i.e. one that blows the 64 MiB size budget) used to be a
construction-time `SyntaxError` and is now a silent never-match at first use.
Node raises no error for such a pattern either. Zero occurrences across the
9,899-pattern sweep.

## Validation

* `cargo test -p perry-runtime --lib -- --test-threads=1`: 2846 passed,
  0 failed, 4 ignored.
* `test-files/test_gap_9163_lazy_regex_semantics.ts` byte-identical to
  `node --experimental-strip-types`.
* `cargo fmt --all -- --check` clean; `check_file_size.sh`,
  `check_thread_locals.py`, `check_test_registration.py`,
  `workspace_architecture.py --check` all OK.
* `cargo check -p perry-runtime --no-default-features` (regex engine gated off)
  clean.
* Match-throughput control fixture (200k `test`, 200k `exec`, 100k
  `replace`, 100k `search` on a built regex): 220 → 198 ms min. The per-op null
  check costs nothing measurable.
* `.text` of a compiled fixture binary 8,530,132 → 8,536,468 bytes (+6,336,
  +0.07%). Codegen is unchanged; the growth is runtime only.

Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP

* chore: classify VALIDATED_PATTERNS and add the changelog fragment

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant