Skip to content

fix(runtime): #5586 — RegExp.prototype.exec reads lastIndex once and honors a non-writable lastIndex - #5769

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5586-regexp-test262
Jun 28, 2026
Merged

fix(runtime): #5586 — RegExp.prototype.exec reads lastIndex once and honors a non-writable lastIndex#5769
proggeramlug merged 1 commit into
mainfrom
worktree-fix-5586-regexp-test262

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Two RegExp.prototype.exec / RegExpBuiltinExec (ECMA-262 22.2.7.2) spec-compliance fixes toward the test262 built-ins/RegExp tracker (#5586):

  1. lastIndex is read exactly once, up front. Step 4 of RegExpBuiltinExec reads lastIndex (GetToLength) before the global/sticky branch (step 8). Perry previously skipped the read entirely for a non-global/non-sticky regex, so a coercible lastIndex (re.lastIndex = { valueOf() {…} }) was never observed. The value still only drives the search start for a stateful regex (and is not written back for a non-stateful one).

  2. A non-writable lastIndex throws. The lastIndex updates use Set(R, "lastIndex", v, true) (Throw=true), so a lastIndex made non-writable via Object.defineProperty must raise a TypeError on a stateful match instead of silently dropping the write. A new set_last_index_throwing helper replaces the raw header write at every stateful write site.

Tests fixed (test262, pinned 4249661)

  • built-ins/RegExp/prototype/exec/success-lastindex-access.js
  • built-ins/RegExp/prototype/exec/failure-lastindex-access.js
  • built-ins/RegExp/prototype/exec/y-fail-lastindex-no-write.js
  • built-ins/RegExp/prototype/test/y-fail-lastindex-no-write.js

Validation

  • built-ins/RegExp/prototype subset: 185 pass, 0 compile-fail, 0 diff; the 4 cases above moved from fail → pass, and the remaining 6 failures are pre-existing categorical gaps unrelated to this change (capture-group semantics, lone-surrogate parse, UTF-16-code-unit u-flag lastIndex, and the propertyIsEnumerable-on-RegExp.prototype dispatch bug).
  • perry-runtime regex unit tests: 30 pass.
  • New node-parity regression test test-files/test_issue_5586_regexp_exec_lastindex.ts (byte-identical under Perry and node --experimental-strip-types); existing regex gap tests still match Node.

Note: version bump + CHANGELOG entry intentionally omitted for the maintainer to fold in at merge time.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved regular expression exec/test behavior to follow standard lastIndex rules more closely.
    • Non-stateful regexes now ignore lastIndex changes, while global and sticky regexes update it correctly after matches and failures.
    • Attempts to update a read-only lastIndex now throw an error instead of failing silently.
  • Tests

    • Added coverage for lastIndex reading, updating, and error handling across common regex scenarios.

…able lastIndex

RegExpBuiltinExec step 4 reads `lastIndex` (Get → ToLength) exactly once, up
front, *before* the global/sticky branch (step 8). Perry previously skipped the
read entirely for a non-global/non-sticky regex, so a coercible `lastIndex`
(`re.lastIndex = { valueOf() {…} }`) was never observed — test262
prototype/exec/{success,failure}-lastindex-access expect exactly one read and
no write-back.

The lastIndex updates also use `Set(R, "lastIndex", v, true)` (Throw=true): a
non-writable `lastIndex` must make a stateful match raise a TypeError instead
of silently dropping the write (prototype/{exec,test}/y-fail-lastindex-no-write).

Adds a `set_last_index_throwing` helper used at every stateful write site, plus
a node-parity regression test. Fixes 4 test262 cases with no regressions in the
built-ins/RegExp/prototype subset (185 pass, 0 compile-fail, 0 diff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds set_last_index_throwing in the regex runtime to throw a TypeError when lastIndex is non-writable. Updates js_regexp_exec to read lastIndex once up-front and force offset to 0 for non-global/non-sticky regexes, replacing all store_last_index_number calls with the new throwing variant. A new test file validates these ECMAScript-conformant semantics.

Changes

RegExp lastIndex ECMAScript conformance

Layer / File(s) Summary
set_last_index_throwing helper
crates/perry-runtime/src/regex.rs
New pub(crate) helper (gated on regex-engine) reads lastIndex writability attribute and either throws a TypeError via js_throw or delegates to store_last_index_number.
js_regexp_exec lastIndex read and throw wiring
crates/perry-runtime/src/regex/exec.rs
Reads lastIndex once up-front into last_index_read; computes search start as 0 for non-global/non-sticky. Replaces all store_last_index_number calls (early-out, fancy-regex success/failure, standard-regex success/failure) with set_last_index_throwing.
lastIndex semantics tests
test-files/test_issue_5586_regexp_exec_lastindex.ts
Covers: single lastIndex read for non-global/non-sticky (via valueOf counter), global regex advancement and reset, and TypeError on non-writable lastIndex for both exec and test.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 A bunny checked the index with care,
"Is lastIndex writable? Let's dare!"
If frozen in place, throw a type-fit,
If not global, just start from the pit.
Now exec reads the spec line by line—
The tests all agree, everything's fine! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main change: RegExp exec lastIndex semantics and non-writable lastIndex handling.
Description check ✅ Passed The description covers the summary, issue reference, and validation, though it doesn't follow the template's section layout exactly.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-5586-regexp-test262

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

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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)
test-files/test_issue_5586_regexp_exec_lastindex.ts (1)

54-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a success-path non-writable lastIndex regression.

These assertions only cover the reset-on-failure throw path. The PR also changed the successful stateful write sites in crates/perry-runtime/src/regex/exec.rs, so a regression there would still pass this file.

➕ Minimal regression to add
+const g2 = /a/g;
+Object.defineProperty(g2, "lastIndex", { writable: false });
+threw = false;
+try {
+  g2.exec("a");
+} catch (e) {
+  threw = e instanceof TypeError;
+}
+ok("global-nonwritable-exec-throws-on-success", threw);
🤖 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 `@test-files/test_issue_5586_regexp_exec_lastindex.ts` around lines 54 - 74,
Add a success-path regression for non-writable lastIndex using the existing
RegExp exec/test cases in test_issue_5586_regexp_exec_lastindex.ts, since the
current sticky exec/test assertions only exercise the failure/reset path. Extend
the same style of checks around y/y2 to cover a successful stateful match, so
changes in RegExpBuiltinExec or the lastIndex write logic in
crates/perry-runtime/src/regex/exec.rs are caught even when the match succeeds.
🤖 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 `@test-files/test_issue_5586_regexp_exec_lastindex.ts`:
- Around line 54-74: Add a success-path regression for non-writable lastIndex
using the existing RegExp exec/test cases in
test_issue_5586_regexp_exec_lastindex.ts, since the current sticky exec/test
assertions only exercise the failure/reset path. Extend the same style of checks
around y/y2 to cover a successful stateful match, so changes in
RegExpBuiltinExec or the lastIndex write logic in
crates/perry-runtime/src/regex/exec.rs are caught even when the match succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 59706019-a2c5-40ea-a003-645a9f367983

📥 Commits

Reviewing files that changed from the base of the PR and between 1f4a2c5 and cd496a8.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/regex.rs
  • crates/perry-runtime/src/regex/exec.rs
  • test-files/test_issue_5586_regexp_exec_lastindex.ts

@proggeramlug
proggeramlug merged commit c368e7b into main Jun 28, 2026
15 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-5586-regexp-test262 branch June 28, 2026 18:36
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