Skip to content

Merge train 177: #10174 - #10187

Merged
proggeramlug merged 5 commits into
mainfrom
train177
Sep 13, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
train177

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Merge train 177: lands #10174 (bind the subject and program once per split/replace/match, and split forward instead of trying every position; part of #10165) at head d61fe85913, plus the workspace version bump to 0.5.1548.

The four PR commits were cherry-picked onto 9b911855f8 without conflicts and match by git patch-id --stable. crates/perry-runtime is byte-identical to the PR head; the tree differs only by main's own commits since the PR's base (HIR JSON-define work) and the bump.

One commit was added to the PR after review, gc: record the forward-split test counter's holder verdict. scripts/gc_runtime_root_holders.py flagged the PR's new #[cfg(test)] FORWARD_SPLITS: Cell<usize> under rule B, and the commit records it as test_only in scripts/gc_runtime_root_holders.json. It is a count and never an address.

Local validation (perrymaster, Linux x86_64; --locked, no publish-age override in the environment):

  • cargo metadata --locked, cargo fmt --all -- --check
  • cargo check -p perry-runtime --no-default-features --features full --lib (regex feature off): 0 warnings
  • cargo check -p perry-runtime --lib --tests: no warnings outside the known global_this_webassembly.rs dead code on main
  • cargo test -p perry-runtime --lib -- --test-threads=1: 3682 passed, 1 failed. The failure is native_stack::tests::stack_top_respects_custom_thread_stack_sizes, red on main.
  • cargo clippy -p perry-runtime --lib --tests: the same 12 approx_constant errors as main at 9b911855f8. Diffed against main, the only change is one type_complexity warning in gc/tests/runtime_roots/perex_split.rs.
  • scripts/run_lint_gates.sh script tier: 76 of 77 pass. The failure is public benchmark evidence freshness, which fails identically on main (main replay: 1 of 77, same gate). The compile tier was not run on this train.
  • check_changeset_fragment.sh PerryTS/perry 10174: pass

Behavioural evidence (spec cases, fallback witnesses, fault injections and measurements) is on #10174. GitHub runners are down, so this local replay is the gate. Next in order: #10176, #10181, #10183.

https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9

Summary by CodeRabbit

  • Performance

    • Improved RegExp split, replace, and global match operations by reducing repeated processing, delivering significantly faster execution on large inputs.
    • Optimized eligible RegExp split operations with forward searching while preserving specification-compatible behavior, including captures, limits, empty matches, and Unicode handling.
  • Testing

    • Added coverage for repeated regular-expression searches, garbage collection, recompilation, changing input strings, and split edge cases.
  • Documentation

    • Updated the documented release version to 0.5.1548.

Ralph Küpper added 5 commits September 13, 2026 10:57
…10165)

String split, replace and global match run a search at every position or
match of one string with one matcher, but execute_with_resources bound both
afresh for each search. Binding a subject decodes the entire string
(perex Input::wtf8, uncharged) and binding a program revalidates every
word, so each of those operations did O(n) binding work per search and
O(n²) overall. On ASCII input this is why `str.split(/[,; ]+/)` took 8.1 s
for a 150,000-unit string.

The three loops already held a BoundSubject over their input. A new
perex_api::Reuse carries it, plus a program binding taken from the
receiver before the loop, into execute_with_resources. Perex's binding
contract allows a binding to outlive allocation, collection and JS
callbacks: Perry's owners hold registered roots and reacquire their base
on every view. A search uses a reused binding only while it is provably
the same object (same string; same receiver still holding the same
program cell); otherwise it binds afresh exactly as before, which covers
an exec override, RegExp.prototype.compile and a different string. Reuse
is built before each loop because runtime handle scopes are a stack.

matchAll's next(), JS-level exec/test and search are one search per JS
call and are unchanged; cross-call reuse is a separate contract.

The non-ASCII seek charge behind the work-limit RangeError (#10164) is on
the Perex side and needs its search-from-position API; this change does
not affect it.

Tests: gc::tests::runtime_roots::perex_reuse covers a whole global loop
with a minor collection at every poll under forced evacuation (subject
and program cell both relocate; fresh work equals reused work plus six
program validations, proving reuse engages), a recompile between
searches, and a different string. Each test fails when its guard is
sabotaged.

Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9
…10165)

RegExp.prototype[@@split] tries a sticky match at every position q.
Each attempt starts a whole search, so split pays a search's fixed setup
per subject unit: about 27.6 work units per unit for `/[,; ]+/`, against
about 9 for a global exec loop over the same subject.

A non-sticky search from q returns the leftmost position s >= q where
the pattern matches, with the same match a sticky attempt at s finds.
The attempts at q..s-1 can therefore be skipped without changing any
piece or capture, and empty matches and Unicode advancement line up.
Skipping them is unobservable only when nothing can see a RegExpExec:

- the species is absent or the intrinsic RegExp (recognised by its call
  thunk), so the splitter is a fresh object no user code holds and its
  skipped lastIndex writes cannot be seen; a user species could return
  a real RegExp and read lastIndex afterwards;
- the splitter's exec resolves, without running a getter, to the builtin
  data property (regexp_view_uses_builtin), so the skipped Get(exec)
  calls cannot be seen either.

When both hold, split compiles a program from the splitter's own internal
source and canonical flags without `y` (never from the receiver, whose
program a limit valueOf could replace via RegExp.prototype.compile after
the splitter was built) and searches forward with it, reusing the
operation's subject binding. Anything else runs the unchanged per-position
sticky loop.

Tests (gc::tests::runtime_roots::perex_split):
- forward search matches ten results derived by hand from the sticky
  algorithm (repeated and unmatched captures, empty matches, `$` at the
  end, limits inside captures, Unicode empty-match advancement, the
  non-ASCII #10164 record), each asserted to take the forward path;
- a user species returning a real RegExp keeps the sticky loop and
  leaves the splitter's lastIndex at 2, as the specification requires;
- the existing species-factory and custom-exec tests now also assert
  the sticky loop ran.
Sabotage: admitting any species fails the user-species test; trying the
end of the input or dropping captures fails the forward-search test.

Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9
gc_runtime_root_holders.py flags the new #[cfg(test)] FORWARD_SPLITS Cell<usize> under rule B. It is a test-only count, never an address.

Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9
@proggeramlug
proggeramlug merged commit 6874a9e into main Sep 13, 2026
31 of 36 checks passed
@proggeramlug
proggeramlug deleted the train177 branch September 13, 2026 11:12
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: d27038a7-89d2-4ac6-87cb-3867ea9a10b9

📥 Commits

Reviewing files that changed from the base of the PR and between 9b91185 and 002610a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10174-regex-bind-once-forward-split.md
  • crates/perry-runtime/src/gc/tests/runtime_roots.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs
  • crates/perry-runtime/src/object/regex_proto_thunks.rs
  • crates/perry-runtime/src/regex/match_all.rs
  • crates/perry-runtime/src/regex/perex_api.rs
  • crates/perry-runtime/src/regex/perex_construct.rs
  • crates/perry-runtime/src/regex/perex_dispatch.rs
  • crates/perry-runtime/src/regex/perex_match_search.rs
  • crates/perry-runtime/src/regex/perex_replace.rs
  • crates/perry-runtime/src/regex/perex_split.rs
  • scripts/gc_runtime_root_holders.json

📝 Walkthrough

Walkthrough

Changes

RegExp performance changes

Layer / File(s) Summary
Reusable RegExp execution bindings
crates/perry-runtime/src/regex/perex_api.rs, crates/perry-runtime/src/regex/perex_dispatch.rs, crates/perry-runtime/src/regex/perex_match_search.rs, crates/perry-runtime/src/regex/perex_replace.rs, crates/perry-runtime/src/regex/match_all.rs, crates/perry-runtime/src/regex/perex_split.rs, crates/perry-runtime/src/gc/tests/runtime_roots/*
Adds Reuse bindings for subjects and compiled programs. Global match, replace, and split execution can pass these bindings across searches. Tests cover moving GC, recompilation, and changed input strings.
Conditional forward split search
crates/perry-runtime/src/object/regex_proto_thunks.rs, crates/perry-runtime/src/regex/perex_construct.rs, crates/perry-runtime/src/regex/perex_split.rs, crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs, scripts/gc_runtime_root_holders.json
Adds intrinsic-constructor detection and non-sticky program construction. Eligible split operations use forward matching; custom species or custom exec cases retain the sticky loop.
Release metadata and performance documentation
Cargo.toml, CLAUDE.md, changelog.d/10174-regex-bind-once-forward-split.md
Updates the version to 0.5.1548 and documents the RegExp performance changes.

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

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant RegExpSplit
  participant forward_program
  participant host_find
  participant StickyLoop
  RegExpSplit->>forward_program: Check constructor and builtin exec
  alt Forward path is eligible
    forward_program-->>RegExpSplit: Return non-sticky program
    RegExpSplit->>host_find: Search from current position
    host_find-->>RegExpSplit: Return captures and match span
  else Forward path is not eligible
    forward_program-->>RegExpSplit: Return no program
    RegExpSplit->>StickyLoop: Execute per-position sticky search
    StickyLoop-->>RegExpSplit: Return match result
  end
Loading
✨ 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 train177

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.

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