Skip to content

fix(runtime): RegExp \w/\W/\b use ASCII word semantics and . excludes all four LineTerminators - #9263

Merged
proggeramlug merged 4 commits into
PerryTS:mainfrom
proggeramlug:fix/9217-9218-regexp-word-dot
Aug 31, 2026
Merged

fix(runtime): RegExp \w/\W/\b use ASCII word semantics and . excludes all four LineTerminators#9263
proggeramlug merged 4 commits into
PerryTS:mainfrom
proggeramlug:fix/9217-9218-regexp-word-dot

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #9217 and #9218 — two silent wrong answers in the JS→Rust regex translation, both of which made patterns match more than the program asked for.

differential before after
focused corpus, 67 patterns × 12 subjects 49 records / 175 cells 11 records / 12 cells
gap fixture, 106 output lines mismatch byte-identical to node

#9217\w, \W, \b, \B were Unicode

Perry emitted Rust's Unicode word-character semantics. ECMAScript defines the word set as ASCII-only [A-Za-z0-9_], and the u flag does not widen it — the only widening is i+u adding U+212A KELVIN SIGN and U+017F LATIN SMALL LETTER LONG S (§22.2.2.9.3 WordCharacters). So /:?[\w-]+/ matched éΩ中, and \b inherited the divergence because word boundaries are defined in terms of \w.

Mixed and negated character classes now preserve JS case-insensitive semantics without letting Rust case-fold the word arm — that separation is what makes [\w\W] and [^\w\W] come out right under i.

#9218. excluded only \n

ECMAScript's . matches any character except a LineTerminator: \n, \r, U+2028, U+2029 (§22.2.2.7 CompileAtom, §12.3). Perry excluded only \n, so "\t\r\n".match(/.{2}/g) returned ["\t\r"] where node returns null.-patterns ran straight across CRLF boundaries. DotAll (s) is unchanged.

#9216's rewrite is preserved

The [^] → (?s:.) and [] → [a&&b] translations that avoid the catastrophic case-fold ([\s\S] under i walked 1,114,112 code points) remain intact, with no full-range class reintroduced. A regression test additionally pins that quantifiers apply atomically to the rewritten negated classes.

Verification

  • The differential harness (scripts/regex_9217_9218_differential.mjs) was run against unchanged main first and confirmed to fail on the cases fixed here.
  • After the fix, zero divergences across: accented Latin, Greek, CJK, Kelvin sign, long s, LF, CR, U+2028, U+2029, and the /.{2}/g CRLF case.
  • Gap fixture test_gap_9217_9218_regexp_word_dot.ts: node and perry produce identical 106-line output (sha256 b989905fdf66e0ed…).
  • cargo test -p perry-runtime --lib -- --test-threads=1: 2,870 passed, 0 failed, 4 ignored. Existing regex tests 66/66.

A correction to the issue reports

The 3,402-pattern corpus differential that produced #9217 and #9218 reported 64 diverging records on main and attributed all of them to these two bugs. That attribution was not quite right. Record 484 — /[ \t]+$/gm against "\t\r\n" — is an independent multiline-$/CRLF divergence, and it is deliberately left unchanged here.

The other residual divergences after this fix are likewise pre-existing and unrelated: non-u UTF-16 code-unit matching for astral characters (emoji), and multiline anchors around CRLF. The fixture uses u for its astral case specifically so it does not conflate those with this change.

Summary by CodeRabbit

  • Bug Fixes

    • Updated regular expressions to match ECMAScript word-character and word-boundary behavior.
    • Corrected . so it excludes all line terminators unless dotAll mode is enabled.
    • Improved handling of Unicode case-insensitive matching, character classes, and empty classes.
  • Tests

    • Added comprehensive compatibility tests comparing regular-expression behavior across supported engines.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c27be9d-c29a-4d2c-a5fc-d2b601e05ba5

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee9919 and 9f9fe62.

📒 Files selected for processing (6)
  • changelog.d/9217-9218-regexp-ascii-word-dot.md
  • crates/perry-runtime/src/regex/grammar.rs
  • crates/perry-runtime/src/regex/lazy.rs
  • crates/perry-runtime/src/regex/tests.rs
  • scripts/regex_9217_9218_differential.mjs
  • test-files/test_gap_9217_9218_regexp_word_dot.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The regex engine now translates \w, \W, \b, \B, and . according to ECMAScript semantics. Flag-aware translation propagates through lazy compilation. Rust tests, a Node.js differential runner, and a gap-test fixture cover the updated behavior.

RegExp semantics

Layer / File(s) Summary
ECMAScript word and dot translation
crates/perry-runtime/src/regex/grammar.rs, changelog.d/9217-9218-regexp-ascii-word-dot.md
The translator uses ASCII word members, adds Kelvin sign and long s for i+u, rewrites boundaries and case-insensitive classes, excludes all four line terminators from non-dotAll ., and preserves empty-class rewrites.
Flag propagation and runtime regression tests
crates/perry-runtime/src/regex/lazy.rs, crates/perry-runtime/src/regex/tests.rs
Lazy regex compilation passes flags to the translator. Tests cover word escapes, boundaries, dot behavior, and the updated legacy escape translation.
Node and Perry differential validation
scripts/regex_9217_9218_differential.mjs, test-files/test_gap_9217_9218_regexp_word_dot.ts
The runner compares Node.js and Perry results for focused or TSV-provided cases. The fixture covers word classes, boundaries, Unicode folding, line terminators, dotAll, and empty classes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 9f9fe

This PR corrects RegExp word-character and line-terminator matching to align with ECMAScript behavior, with focused regression coverage and no actionable merge-blocking risk remaining after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant DifferentialRunner
  participant NodeJS
  participant PerryBinary
  DifferentialRunner->>NodeJS: Run generated TypeScript regex probe
  NodeJS-->>DifferentialRunner: Serialize pattern results
  DifferentialRunner->>PerryBinary: Run the same probe
  PerryBinary-->>DifferentialRunner: Serialize pattern results
  DifferentialRunner->>DifferentialRunner: Compare records and report divergences
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The supplied linked issue covers the \\w, \\W, \\b, and \\B semantics in issue #9217. The pull request also changes dot and LineTerminator semantics for issue #9218, but issue #9218 is not include… Link issue #9218 in the pull request context, or move the dot and LineTerminator changes into a separate pull request. Keep this pull request limited to the requirements covered by the supplied linked issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 65.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 23 functions across 5 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the two primary regex translation fixes: ECMAScript word semantics and LineTerminator handling for ..
Description check ✅ Passed The description provides a clear summary, detailed changes, related issue references, verification commands and results, regression coverage, and scope notes. It does not reproduce every template head…
Linked Issues check ✅ Passed The implementation satisfies issue #9217 by using the ECMAScript ASCII word set, handling the i plus u Kelvin sign and long s additions, preserving negated-class behavior, and updating word-bounda…
Full details: Description check

Explanation

The description provides a clear summary, detailed changes, related issue references, verification commands and results, regression coverage, and scope notes. It does not reproduce every template heading or checklist item, but the required information is mostly complete.

Full details: Linked Issues check

Explanation

The implementation satisfies issue #9217 by using the ECMAScript ASCII word set, handling the i plus u Kelvin sign and long s additions, preserving negated-class behavior, and updating word-boundary translation and tests.

Full details: Out of Scope Changes check

Explanation

The supplied linked issue covers the \w, \W, \b, and \B semantics in issue #9217. The pull request also changes dot and LineTerminator semantics for issue #9218, but issue #9218 is not included in the provided linked-issues context.

Full details: Docstring Coverage

Explanation

Docstring coverage is 65.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 23 functions across 5 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ 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.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merged.

Validated on a shared branch with #9228, #9257, #9263, #9271, #9272, #9274, #9277, #9279 and #9280 — one build, one validation pass, then split back out and merged individually.

Results across the batch:

  • perry-runtime 2885 passed / 0 failed at RUST_TEST_THREADS=1
  • perry-codegen 30 suites green (the one failure was a doc-test reporting a missing libperry_codegen-*.rlib — an artifact of my own cleanup of stale build dirs, confirmed by a clean re-run at 31/31, not a code defect)
  • all 60 lint gates plus the check_thread_locals / tls_budget checkers
  • a nine-row differential probe byte-identical to node 26.5.1, covering every changed area: RegExp \w/\b/. ASCII and LineTerminator semantics, offset (a[i±1]) and length-bounded array reads, the assert RegExp matcher, and closure identity across a 40k-allocation GC churn
  • seven earlier regression probes re-run at zero diff lines: tagged and scalar array stores, pointer↔scalar transition churn, growth-forwarding receivers, BigInt negation, iterator protocols, field shadowing

One probe (protorepl) moved from 2 to 4 diff lines and I ran it down rather than waving it through: both divergences are accepted trades already on main — the fresh-instance case (#9239) and #9247's deliberate change of a custom-chain miss from Some(undefined) to None, which it made because swallowing the miss left everything Perry synthesizes unreachable (Object(true).valueOf(), plain-function .prototype, iterator helpers). Neither is anything in this batch.

@proggeramlug
proggeramlug merged commit cdb1f13 into PerryTS:main Aug 31, 2026
29 checks passed
proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Aug 31, 2026
…erryTS#9305 fallout)

The transport fix unmasked this second regression: js_regex_to_rust
spells ECMAScript's ASCII \b/\B as (?-iu:\b) (PerryTS#9263), which the regex
crate accepts but fancy-regex rejects (NonUnicodeUnsupported). Any
lookaround/backreference pattern with a word boundary was a SyntaxError
— cli.js's marked html-block regex among them; its throw inside a
microtask was the longjmp that the miscompiled runner turned into the
--help SIGSEGV. build_fancy_regex now rewrites the translator's marker
(unambiguous — '(?-iu:' cannot survive from user input) into the
one-char-lookaround boundary spelling the i+u path already uses.

Claude-Session: https://claude.ai/code/session_014knX724SYDogwzsXybCGxp
proggeramlug added a commit that referenced this pull request Aug 31, 2026
…ault fixed, parity gate back online (#9305) (#9323)

* runtime: route every longjmp-target setjmp through a C trampoline (#9305)

rustc cannot express returns_twice, so a raw setjmp in a Rust frame is
compiled under LLVM's one-return assumption — stack slots live only into
the longjmp path get colored into unrelated normal-path temporaries.
run_microtasks crashed exactly this way (cached TLS base spill reused by
the task-record copy loop). No Rust frame is a longjmp target anymore:
perry_sjlj_try (C, compiled with real setjmp semantics) is the only
twice-returning frame, and Rust callers use exception::arm_trap_and_run /
catch_js_throw. The one remaining raw setjmp (gc/roots.rs register
snapshot) never longjmps and is documented as such.

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

* tests: #9305 regression fixture (throw-in-microtask) + transport unit tests

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

* fmt + changelog fragment (#9305)

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

* docs: js_try_push / ffi::setjmp contract notes (#9305)

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

* stdlib+ext-fastify: convert remaining raw setjmp trap sites to the C trampoline (#9305)

Same hazard class as the runtime sites: raw setjmp in a Rust frame is
compiled without returns_twice. perry-stdlib goes through
exception::catch_js_throw; perry-ext-fastify (no Cargo dep on
perry-runtime by design) declares the perry_sjlj_try C symbol directly
and mirrors arm_trap_and_run locally.

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

* runtime(regex): fancy engine accepts the ASCII word-boundary marker (#9305 fallout)

The transport fix unmasked this second regression: js_regex_to_rust
spells ECMAScript's ASCII \b/\B as (?-iu:\b) (#9263), which the regex
crate accepts but fancy-regex rejects (NonUnicodeUnsupported). Any
lookaround/backreference pattern with a word boundary was a SyntaxError
— cli.js's marked html-block regex among them; its throw inside a
microtask was the longjmp that the miscompiled runner turned into the
--help SIGSEGV. build_fancy_regex now rewrites the translator's marker
(unambiguous — '(?-iu:' cannot survive from user input) into the
one-char-lookaround boundary spelling the i+u path already uses.

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

---------

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.

RegExp \w, \W and \b use Unicode word semantics where JS is ASCII-only — /[\w-]+/ matches "éΩ中"

1 participant