diff --git a/CLAUDE.md b/CLAUDE.md index 0c3b42d9d6..2f56f10f27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1547 +**Current Version:** 0.5.1548 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 3a7fe139e6..da894067f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5690,7 +5690,7 @@ checksum = "4b2094dda4d997bf73a372cb660d02e9abdb0a13cbf834ddd2b2f8847bffe2cf" [[package]] name = "perry" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "base64 0.22.1", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-dispatch", "serde", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "cc", "libc", @@ -5771,7 +5771,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "aho-corasick", "anyhow", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-hir", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-hir", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-dispatch", @@ -5814,7 +5814,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-hir", @@ -5822,7 +5822,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "base64 0.22.1", @@ -5834,7 +5834,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-hir", @@ -5842,7 +5842,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "async-trait", @@ -5870,14 +5870,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "serde", "serde_json", @@ -5885,7 +5885,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1547" +version = "0.5.1548" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "clap", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "block2", "objc2", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "argon2", "perry-ffi", @@ -5930,7 +5930,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "reqwest", @@ -5939,7 +5939,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "bcrypt", "perry-ffi", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "rusqlite", @@ -5955,7 +5955,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "scraper", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-runtime", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "chrono", "cron", @@ -5981,7 +5981,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "chrono", "perry-ffi", @@ -5989,7 +5989,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "rust_decimal", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "serde_json", @@ -6005,7 +6005,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-runtime", @@ -6021,14 +6021,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "bytes", "http-body-util", @@ -6046,7 +6046,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "bytes", "lazy_static", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "bytes", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "lazy_static", "perry-ffi", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "lru", "perry-ffi", @@ -6121,7 +6121,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "chrono", "perry-ffi", @@ -6129,7 +6129,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "bson", "futures-util", @@ -6141,7 +6141,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "chrono", "perry-ffi", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "nanoid", "perry-ffi", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "bytes", "perry-ffi", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6196,7 +6196,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "lettre", "perry-ffi", @@ -6206,7 +6206,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "notify", "perry-ffi", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "printpdf", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "sqlx", @@ -6235,7 +6235,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-runtime", @@ -6244,7 +6244,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "governor", "perry-ffi", @@ -6252,7 +6252,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "fast_image_resize", "image", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "lazy_static", "perry-ffi", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-ffi", @@ -6292,7 +6292,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-runtime", @@ -6301,7 +6301,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "uuid", @@ -6309,7 +6309,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-validation", @@ -6318,7 +6318,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "futures-util", "lazy_static", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "brotli", "flate2", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6351,7 +6351,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-api-manifest", @@ -6372,11 +6372,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1547" +version = "0.5.1548" [[package]] name = "perry-parser" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-diagnostics", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perex", "regex", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "ahash", "anyhow", @@ -6458,14 +6458,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6560,14 +6560,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "perry-hir", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6584,7 +6584,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "itoa", @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "rand 0.10.2", "serde", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "block2", @@ -6652,7 +6652,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "block2", @@ -6669,7 +6669,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1547" +version = "0.5.1548" [[package]] name = "perry-ui-test" @@ -6680,11 +6680,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1547" +version = "0.5.1548" [[package]] name = "perry-ui-tvos" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "block2", @@ -6701,7 +6701,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "block2", @@ -6718,7 +6718,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "block2", "libc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "libc", @@ -6751,7 +6751,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "base64 0.22.1", "libc", @@ -6764,7 +6764,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "anyhow", "base64 0.22.1", @@ -6780,7 +6780,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "idna", "regex", @@ -6790,7 +6790,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1547" +version = "0.5.1548" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index fa4f1655bf..022d212597 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1547" +version = "0.5.1548" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10174-regex-bind-once-forward-split.md b/changelog.d/10174-regex-bind-once-forward-split.md new file mode 100644 index 0000000000..d0a641b80c --- /dev/null +++ b/changelog.d/10174-regex-bind-once-forward-split.md @@ -0,0 +1,3 @@ +### Performance + +- **RegExp `split`, `replace` and global `match` bind the subject and program once per operation, and `split` searches forward** (#10165). Each search used to decode the whole string and revalidate the whole program, so these operations did quadratic work: an ASCII `split(/[,; ]+/)` of 150,000 units took 8.1 s. They are now linear. `split` also searches for the next match instead of trying a sticky match at every position, whenever nothing can observe the difference (an absent or intrinsic `RegExp` species and the builtin `exec`), which brings ASCII split to about 13× Node from about 220×. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index 58feac5ab8..d21d61fe5c 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -37,6 +37,8 @@ mod perex_public; #[cfg(feature = "regex-engine")] mod perex_replace; #[cfg(feature = "regex-engine")] +mod perex_reuse; +#[cfg(feature = "regex-engine")] mod perex_split; #[cfg(feature = "regex-engine")] mod perex_strings; diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs index 0c38320d46..2c4438effd 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs @@ -238,6 +238,7 @@ fn perex_dispatch_getter_and_callback_reacquire_original_input_after_gc() { &mut Budget::new(api::WORK), &MemoryBudget::new(api::SCRATCH_BYTES), &mut crate::regex::perex_runtime::poll, + None, )) .unwrap() .object(); @@ -349,7 +350,8 @@ fn perex_dispatch_validates_override_results_and_keeps_one_work_allowance() { false, &mut budget, &memory, - &mut crate::regex::perex_runtime::poll + &mut crate::regex::perex_runtime::poll, + None, )) .is_some()); assert_eq!(budget.remaining(), expected); @@ -361,7 +363,8 @@ fn perex_dispatch_validates_override_results_and_keeps_one_work_allowance() { false, &mut budget, &memory, - &mut crate::regex::perex_runtime::poll + &mut crate::regex::perex_runtime::poll, + None, ), Err(EngineError::Execution( perex::executor::ExecError::WorkLimit diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs new file mode 100644 index 0000000000..9050c8c439 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs @@ -0,0 +1,226 @@ +//! Compound-operation binding reuse (#10165): one subject and program binding +//! serves every search of an operation, across actual moving collections, and +//! is abandoned whenever the receiver's program or the string is not the one +//! it bound. +use super::*; +use crate::array::ArrayHeader; +use crate::regex::perex_api::{self as api, Reuse}; +use crate::regex::perex_memory::MemoryBudget; +use crate::regex::perex_owner::HeapSubject; +use crate::regex::RegExpHeader; +use crate::string::StringHeader; +use crate::value::{js_nanbox_pointer, js_nanbox_string}; +use perex::binding::BoundSubject; +use perex::Budget; + +fn text<'s>(scope: &'s RuntimeHandleScope, bytes: &[u8]) -> RuntimeHandle<'s> { + scope.root_string_ptr(crate::string::js_string_from_bytes( + bytes.as_ptr(), + bytes.len() as u32, + )) +} + +/// A NaN-boxed receiver handle, as split/replace/match root their receivers. +fn regex<'s>(scope: &'s RuntimeHandleScope, pattern: &str, flags: &str) -> RuntimeHandle<'s> { + let pattern = text(scope, pattern.as_bytes()); + let flags = text(scope, flags.as_bytes()); + let re = pattern.with_const_ptr::(|pattern| { + flags.with_const_ptr::(|flags| crate::regex::js_regexp_new(pattern, flags)) + }); + scope.root_nanbox_f64(js_nanbox_pointer(re as i64)) +} + +fn receiver_ptr(receiver: &RuntimeHandle<'_>) -> *mut RegExpHeader { + crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as *mut RegExpHeader +} + +fn first_item(scope: &RuntimeHandleScope, array: *mut ArrayHeader) -> Vec { + let array = scope.root_raw_mut_ptr(array); + let value = array.with_const_ptr::(|a| crate::array::js_array_get_f64(a, 0)); + let mut scratch = [0; crate::value::SHORT_STRING_MAX_LEN]; + let (data, len) = crate::string::str_bytes_from_jsvalue(value, &mut scratch).unwrap(); + unsafe { std::slice::from_raw_parts(data, len as usize).to_vec() } +} + +/// Run a global exec loop to exhaustion with a collection at every poll, +/// returning each full match and the work the loop charged. +fn global_loop( + receiver: &RuntimeHandle<'_>, + input: &RuntimeHandle<'_>, + reuse: Option<&Reuse<'_, '_>>, +) -> (Vec>, usize) { + let memory = MemoryBudget::new(api::SCRATCH_BYTES); + let mut budget = Budget::new(api::WORK); + let mut matches = Vec::new(); + let roots = RuntimeHandleScope::active_len_for_tests(); + loop { + let iteration = RuntimeHandleScope::new(); + // Re-read both addresses every search: the previous one collected. + let found = input + .with_const_ptr::(|input| { + api::execute_with_resources( + receiver_ptr(receiver), + input, + true, + &mut budget, + &memory, + &mut || { + gc_collect_minor(); + Ok(()) + }, + reuse, + ) + }) + .unwrap(); + let Some(found) = found else { break }; + matches.push(first_item(&iteration, found.array)); + drop(iteration); + assert_eq!(RuntimeHandleScope::active_len_for_tests(), roots); + } + (matches, api::WORK - budget.remaining()) +} + +#[test] +fn perex_reuse_serves_a_whole_global_loop_across_moving_collections() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + // Non-ASCII storage (the byte representation, not the ASCII layout). Every + // object is allocated immediately before its loop so it is still young + // and the loop's collections must actually relocate it. + const SUBJECT: &str = "ä1 b22 c333 ä4444 é55555"; + const PATTERN: &str = "[a-zäé]+\\d+"; + let expected: Vec> = ["ä1", "b22", "c333", "ä4444", "é55555"] + .iter() + .map(|s| s.as_bytes().to_vec()) + .collect(); + + let input = text(&scope, SUBJECT.as_bytes()); + let reused = regex(&scope, PATTERN, "gu"); + let subject = BoundSubject::new(unsafe { HeapSubject::new(input) }.unwrap()).unwrap(); + let mut setup = Budget::new(api::WORK); + let reuse = Reuse::new(&scope, &reused, input, &subject, &mut setup); + let input_before = input.with_const_ptr::(|p| p as usize); + let program_before = unsafe { (*receiver_ptr(&reused)).perex_program as usize }; + let cycles = copying_minor_cycles(); + let (reused_matches, reused_work) = global_loop(&reused, &input, Some(&reuse)); + + assert_eq!(reused_matches, expected); + assert!( + copying_minor_cycles() > cycles, + "the loop must actually collect" + ); + assert_ne!( + input.with_const_ptr::(|p| p as usize), + input_before, + "the bound subject must have been relocated during the loop" + ); + assert_ne!( + unsafe { (*receiver_ptr(&reused)).perex_program as usize }, + program_before, + "the reused program must have moved, and still be recognised as the same cell" + ); + + // The same operation on identical, independent objects without reuse. + let fresh_input = text(&scope, SUBJECT.as_bytes()); + let fresh = regex(&scope, PATTERN, "gu"); + let (fresh_matches, fresh_work) = global_loop(&fresh, &fresh_input, None); + assert_eq!(fresh_matches, expected); + // Six searches (five matches and the final miss). Binding per search charges + // program validation six times; reuse charged it once, in `setup`. + let validation = api::WORK - setup.remaining(); + assert!(validation > 0); + assert_eq!(fresh_work, reused_work + 6 * validation); +} + +#[test] +fn perex_reuse_uses_the_receivers_current_program_after_recompile() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let input = text(&scope, b"aaa bbb aaa"); + let receiver = regex(&scope, "a+", "g"); + let subject = BoundSubject::new(unsafe { HeapSubject::new(input) }.unwrap()).unwrap(); + let reuse = Reuse::new( + &scope, + &receiver, + input, + &subject, + &mut Budget::new(api::WORK), + ); + let search = |scope: &RuntimeHandleScope| { + let memory = MemoryBudget::new(api::SCRATCH_BYTES); + input + .with_const_ptr::(|s| { + api::execute_with_resources( + receiver_ptr(&receiver), + s, + true, + &mut Budget::new(api::WORK), + &memory, + &mut || { + gc_collect_minor(); + Ok(()) + }, + Some(&reuse), + ) + }) + .unwrap() + .map(|found| first_item(scope, found.array)) + }; + let first = RuntimeHandleScope::new(); + assert_eq!(search(&first).as_deref(), Some(&b"aaa"[..])); + drop(first); + // RegExp.prototype.compile publishes a new program and resets lastIndex. + let pattern = text(&scope, b"b+"); + let flags = text(&scope, b"g"); + crate::regex::js_regexp_compile_value( + receiver_ptr(&receiver), + pattern.with_const_ptr::(|p| js_nanbox_string(p as i64)), + flags.with_const_ptr::(|p| js_nanbox_string(p as i64)), + ); + let second = RuntimeHandleScope::new(); + assert_eq!(search(&second).as_deref(), Some(&b"bbb"[..])); +} + +#[test] +fn perex_reuse_binds_a_different_string_afresh() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let bound_input = text(&scope, b"x1"); + let other_input = text(&scope, b"yy22"); + let receiver = regex(&scope, "\\d+", ""); + let subject = BoundSubject::new(unsafe { HeapSubject::new(bound_input) }.unwrap()).unwrap(); + let reuse = Reuse::new( + &scope, + &receiver, + bound_input, + &subject, + &mut Budget::new(api::WORK), + ); + let memory = MemoryBudget::new(api::SCRATCH_BYTES); + let found = other_input + .with_const_ptr::(|s| { + api::execute_with_resources( + receiver_ptr(&receiver), + s, + true, + &mut Budget::new(api::WORK), + &memory, + &mut || Ok(()), + Some(&reuse), + ) + }) + .unwrap() + .unwrap(); + assert_eq!(first_item(&scope, found.array), b"22"); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs index d66dcc84f2..142cec6c40 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_split.rs @@ -641,6 +641,7 @@ fn perex_split_species_order_zero_limit_and_empty_input() { let f = function(&scope, throwing as *const u8, 0); getter(&receiver, b"lastIndex", &f); getter(&matcher, b"lastIndex", &f); + let forward_before = forward_splits(); for (number, expected, count) in [(0.0, 123456, 0.0), (1.0, 1234567, 1.0)] { ORDER.with(|o| o.set(0)); put(&lim, b"number", number); @@ -653,6 +654,11 @@ fn perex_split_species_order_zero_limit_and_empty_input() { assert_eq!(get(&out, b"length"), count); assert_eq!(bytes(get(&matcher, b"seenFlags")), b"vy"); } + assert_eq!( + forward_splits(), + forward_before, + "a species factory must keep the per-position sticky loop" + ); } extern "C" fn custom_exec(c: *const crate::closure::ClosureHeader, input: f64) -> f64 { @@ -716,6 +722,7 @@ fn perex_split_custom_exec_capture_values_reentrancy_and_limit_short_circuit() { getter(&result, b"0", &throws); getter(&result, b"index", &throws); put(&capture, b"toString", throws.get_nanbox_f64()); + let forward_before = forward_splits(); let before = input.get_nanbox_f64().to_bits(); let out = scope.root_nanbox_f64(api::finish(split::regexp( re.get_nanbox_f64(), @@ -735,6 +742,11 @@ fn perex_split_custom_exec_capture_values_reentrancy_and_limit_short_circuit() { 1.0, ))); check(&out, &[Some(b"a")]); + assert_eq!( + forward_splits(), + forward_before, + "a custom exec must keep the per-position sticky loop" + ); } extern "C" fn throwing_hook(_: *const crate::closure::ClosureHeader, _: f64, _: f64) -> f64 { @@ -1141,3 +1153,137 @@ fn perex_numeric_arguments_reject_bigint_after_observable_primitive_conversion() Err(crate::regex::perex_runtime::EngineError::Type(_)) )); } + +fn forward_splits() -> usize { + split::FORWARD_SPLITS.with(Cell::get) +} + +/// Split's forward search (#10165) returns exactly the specification's +/// per-position sticky result. Each expectation below was derived by running +/// the sticky algorithm by hand, not by observing either implementation. +#[test] +fn perex_split_forward_search_matches_the_sticky_specification() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + super::perex_public::register_host_roots(); + type Parts = &'static [Option<&'static [u8]>]; + let cases: &[(&[u8], &str, &[u8], f64, Parts)] = &[ + // A repeated group keeps its last iteration. + ( + b"a1b22c", + r"(\d)+", + b"", + -1.0, + &[Some(b"a"), Some(b"1"), Some(b"b"), Some(b"2"), Some(b"c")], + ), + // An unmatched group is undefined, and a match at the end leaves "". + (b"ab", r"(x)?b", b"", -1.0, &[Some(b"a"), None, Some(b"")]), + // Empty matches everywhere: every position is stepped past once. + ( + b"abc", + "x*", + b"", + -1.0, + &[Some(b"a"), Some(b"b"), Some(b"c")], + ), + // Empty at 0, a real match at 1, empty again at 2. + (b"abc", "b*", b"", -1.0, &[Some(b"a"), Some(b"c")]), + (b",a,", ",", b"", -1.0, &[Some(b""), Some(b"a"), Some(b"")]), + // `$` matches only at the end, which the sticky loop never tries. + (b"ab", "$", b"", -1.0, &[Some(b"ab")]), + (b"a,b,c", ",", b"", 2.0, &[Some(b"a"), Some(b"b")]), + // The limit can fall inside a match's captures. + ( + b"a1b2c3", + r"(\d)", + b"", + 3.0, + &[Some(b"a"), Some(b"1"), Some(b"b")], + ), + // Unicode mode advances an empty match by a whole code point. + ( + "😀😀".as_bytes(), + "", + b"u", + -1.0, + &[Some(b"\xf0\x9f\x98\x80"), Some(b"\xf0\x9f\x98\x80")], + ), + ( + "ä中12,Ö漢345;ef6😀".as_bytes(), + "[,;😀]+", + b"u", + -1.0, + &[ + Some(b"\xc3\xa4\xe4\xb8\xad12"), + Some(b"\xc3\x96\xe6\xbc\xa2345"), + Some(b"ef6"), + Some(b""), + ], + ), + ]; + for (index, (subject, pattern, flags, limit, expected)) in cases.iter().enumerate() { + let local = RuntimeHandleScope::new(); + let input = text(&local, subject); + let separator = regex(&local, pattern.as_bytes(), flags); + let before = forward_splits(); + let out = run(&local, &input, &separator, *limit); + assert_eq!( + forward_splits(), + before + 1, + "case {index} must take the forward search" + ); + check(&out, expected); + } +} + +/// A user species constructor that builds a genuine RegExp and keeps it where +/// JavaScript can reach it afterwards, as any user factory could. +extern "C" fn recording_regexp_species( + _: *const crate::closure::ClosureHeader, + receiver: f64, + flags: f64, +) -> f64 { + let scope = RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let flags = scope.root_nanbox_f64(flags); + let splitter = scope.root_nanbox_f64(js_nanbox_pointer(crate::regex::js_regexp_construct( + receiver.get_nanbox_f64(), + flags.get_nanbox_f64(), + ) as i64)); + put(&receiver, b"splitter", splitter.get_nanbox_f64()); + splitter.get_nanbox_f64() +} + +/// The species condition is what keeps the forward search unobservable: a user +/// species can return a real RegExp with the builtin exec, which passes every +/// other admission check, and still hold the splitter and read what the +/// per-position loop wrote to it. +#[test] +fn perex_split_user_species_regexp_keeps_the_observable_sticky_loop() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let re = regex(&scope, b",", b""); + let holder = object(&scope); + let species = function(&scope, recording_regexp_species as *const u8, 2); + symbol(&holder, "species", species.get_nanbox_f64()); + put(&re, b"constructor", holder.get_nanbox_f64()); + let input = text(&scope, b"a,"); + let before = forward_splits(); + let out = run(&scope, &input, &re, -1.0); + check(&out, &[Some(b"a"), Some(b"")]); + assert_eq!( + forward_splits(), + before, + "a user species must keep the per-position sticky loop" + ); + // The sticky loop's last RegExpExec matched "," at 1 and left lastIndex at + // 2; a forward search would never have written it and left 0. + let splitter = scope.root_nanbox_f64(get(&re, b"splitter")); + assert_eq!(get(&splitter, b"lastIndex"), 2.0); +} diff --git a/crates/perry-runtime/src/object/regex_proto_thunks.rs b/crates/perry-runtime/src/object/regex_proto_thunks.rs index 9ffc0ce0c9..1d1203ba95 100644 --- a/crates/perry-runtime/src/object/regex_proto_thunks.rs +++ b/crates/perry-runtime/src/object/regex_proto_thunks.rs @@ -491,6 +491,17 @@ pub(crate) fn regexp_prototype_test_is_canonical(value: f64) -> bool { }) } +#[cfg(feature = "regex-engine")] +/// The intrinsic `RegExp` constructor, recognised the way the class registry +/// recognises it: by its dedicated call thunk. A subclass, a bound function or +/// a proxy has a different function pointer. +pub(crate) fn is_intrinsic_regexp_constructor(value: f64) -> bool { + let closure = + crate::value::js_nanbox_get_pointer(value) as *const crate::closure::ClosureHeader; + crate::closure::get_valid_func_ptr(closure) + == super::global_this::regexp_constructor_call_thunk as *const u8 +} + /// Non-observable admission for a substring view. An exec/test accessor or /// override must run once on the materialized JS argument, so never invoke /// one while deciding whether to take this optimization. diff --git a/crates/perry-runtime/src/regex/match_all.rs b/crates/perry-runtime/src/regex/match_all.rs index 65bca84ef2..5c3ff102b6 100644 --- a/crates/perry-runtime/src/regex/match_all.rs +++ b/crates/perry-runtime/src/regex/match_all.rs @@ -232,6 +232,7 @@ fn next(iter: *mut ObjectHeader) -> Result { &mut budget, &memory, &mut host::poll, + None, )?; let Some(found) = found else { complete(&iter); diff --git a/crates/perry-runtime/src/regex/perex_api.rs b/crates/perry-runtime/src/regex/perex_api.rs index c14d1cf097..900e603280 100644 --- a/crates/perry-runtime/src/regex/perex_api.rs +++ b/crates/perry-runtime/src/regex/perex_api.rs @@ -97,6 +97,86 @@ pub(crate) fn program<'s>( BoundProgram::new(owner, budget).map_err(|e| EngineError::Program(e.error)) } +/// Bindings one compound operation reuses across its searches (#10165). +/// +/// Split, replace and global match run many searches over one string with one +/// matcher. Binding per search decodes the entire subject and revalidates the +/// entire program every time, which made those loops quadratic in the input. +/// Perex's binding contract lets a binding outlive allocation, collection and +/// JS callbacks: both owners hold registered roots and reacquire their base on +/// every view, so no search needs to rebind because the collector moved them. +/// +/// Build it before the operation's loop. Runtime handle scopes are a stack, so +/// its roots must sit below every per-iteration scope; nothing here roots +/// lazily. A search uses a binding only while it is provably the same object: +/// the same string, and the same receiver still holding the same program cell. +/// Anything else (an `exec` override, a recompiled receiver, another string) +/// binds afresh for that search exactly as before. +pub(crate) struct Reuse<'b, 's> { + input: RuntimeHandle<'s>, + subject: &'b BoundSubject>, + program: Option>, +} + +struct ReusedProgram<'s> { + receiver: RuntimeHandle<'s>, + cell: RuntimeHandle<'s>, + bound: BoundProgram>, +} + +impl<'b, 's> Reuse<'b, 's> { + /// `subject` must bind the whole of `input` (not a window), as the + /// operations' own `subject(input)` bindings do. + pub(crate) fn new( + scope: &'s RuntimeHandleScope, + receiver: &RuntimeHandle<'_>, + input: RuntimeHandle<'s>, + subject: &'b BoundSubject>, + budget: &mut Budget, + ) -> Self { + let re = + crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as *const RegExpHeader; + // A receiver that is not a RegExp with a published program runs no + // builtin search here; its failure belongs to the ordinary path. + let program = (super::is_valid_regex_ptr(re) && unsafe { !(*re).perex_program.is_null() }) + .then(|| { + // Rooting pushes a handle slot and never collects, so `re` and + // its program edge are still current for every read below. + let receiver = scope.root_raw_const_ptr(re); + let cell = scope.root_raw_const_ptr(unsafe { (*re).perex_program }); + let owner = unsafe { GcProgram::from_receiver(scope, &receiver) }.ok()?; + let bound = BoundProgram::new(owner, budget).ok()?; + Some(ReusedProgram { + receiver, + cell, + bound, + }) + }) + .flatten(); + Self { + input, + subject, + program, + } + } + + fn subject_for(&self, input: &RuntimeHandle<'_>) -> Option<&BoundSubject>> { + let current = input.with_const_ptr::(|p| p); + let bound = self.input.with_const_ptr::(|p| p); + (current == bound).then_some(self.subject) + } + + /// Both roots are live, so equal addresses name the same objects even after + /// either moved; a replaced program cannot reuse a cell this root retains. + fn program_for(&self, receiver: &RuntimeHandle<'_>) -> Option<&BoundProgram>> { + let reused = self.program.as_ref()?; + let current = receiver.with_const_ptr::(|p| p); + let bound = reused.receiver.with_const_ptr::(|p| p); + let cell = reused.cell.with_const_ptr::(|p| p); + (current == bound && unsafe { (*current).perex_program } == cell).then_some(&reused.bound) + } +} + pub(crate) struct ExecMatch { pub(crate) full: Span, pub(crate) array: *mut crate::array::ArrayHeader, @@ -186,12 +266,13 @@ pub(crate) fn execute( &mut Budget::new(WORK), &MemoryBudget::new(SCRATCH_BYTES), poll, + None, ) } /// Compound String operations keep one allowance across successive matches. /// Each execution has its own root scope, so a global loop cannot retain a -/// root for every previous result. +/// root for every previous result. `reuse` carries the operation's bindings. pub(crate) fn execute_with_resources( receiver: *mut RegExpHeader, input: *const StringHeader, @@ -199,6 +280,7 @@ pub(crate) fn execute_with_resources( budget: &mut Budget, memory: &MemoryBudget, poll: &mut impl FnMut() -> Result<(), EngineError>, + reuse: Option<&Reuse<'_, '_>>, ) -> Result, EngineError> { let scope = RuntimeHandleScope::new(); let receiver = scope.root_raw_mut_ptr(receiver); @@ -217,15 +299,29 @@ pub(crate) fn execute_with_resources( } return Ok(None); } - let program = program(&scope, &receiver, budget, memory, poll)?; - let subject = BoundSubject::new( - unsafe { HeapSubject::new(input) } - .map_err(|e| EngineError::Subject(perex::binding::SubjectError::Resource(e)))?, - ) - .map_err(|e| EngineError::Subject(e.error))?; + let fresh_program; + let program = match reuse.and_then(|reuse| reuse.program_for(&receiver)) { + Some(program) => program, + None => { + fresh_program = program(&scope, &receiver, budget, memory, poll)?; + &fresh_program + } + }; + let fresh_subject; + let subject = match reuse.and_then(|reuse| reuse.subject_for(&input)) { + Some(subject) => subject, + None => { + fresh_subject = + BoundSubject::new(unsafe { HeapSubject::new(input) }.map_err(|e| { + EngineError::Subject(perex::binding::SubjectError::Resource(e)) + })?) + .map_err(|e| EngineError::Subject(e.error))?; + &fresh_subject + } + }; let found = host::find( - &program, - &subject, + program, + subject, start, if materialize { CaptureMode::All @@ -252,8 +348,8 @@ pub(crate) fn execute_with_resources( caught(|| { super::perex_results::materialize( &input, - &subject, - &program, + subject, + program, &found, has_indices, budget, diff --git a/crates/perry-runtime/src/regex/perex_construct.rs b/crates/perry-runtime/src/regex/perex_construct.rs index aa7def894b..b13d326e41 100644 --- a/crates/perry-runtime/src/regex/perex_construct.rs +++ b/crates/perry-runtime/src/regex/perex_construct.rs @@ -60,6 +60,30 @@ fn compile<'s>( ) } +/// A program for `re`'s own source and canonical flags with `y` removed, +/// compiled from its internal slots, so no property of `re` is observed. +/// Split's forward search uses it in place of the sticky splitter (#10165). +pub(crate) fn nonsticky_program<'s>( + scope: &'s RuntimeHandleScope, + re: &RuntimeHandle<'_>, +) -> Result, EngineError> { + let (source, flags) = + re.with_const_ptr::(|re| unsafe { ((*re).pattern_ptr, (*re).flags_ptr) }); + if source.is_null() || flags.is_null() { + return Err(EngineError::InvalidFlags); + } + let source = scope.root_string_ptr(source); + let flags = scope.root_string_ptr(flags); + let canonical = unsafe { + flags.with_string_bytes(|bytes| { + let without: Vec = bytes.iter().copied().filter(|&b| b != b'y').collect(); + CanonicalFlags::parse(&without) + }) + } + .ok_or(EngineError::InvalidFlags)?; + compile(scope, source, canonical) +} + unsafe fn publish( receiver: &RuntimeHandle<'_>, source: &RuntimeHandle<'_>, diff --git a/crates/perry-runtime/src/regex/perex_dispatch.rs b/crates/perry-runtime/src/regex/perex_dispatch.rs index 14fc0aa6e1..e6f17c5a43 100644 --- a/crates/perry-runtime/src/regex/perex_dispatch.rs +++ b/crates/perry-runtime/src/regex/perex_dispatch.rs @@ -97,6 +97,7 @@ pub(crate) fn call_one( /// RegExpExec with operation-owned limits. Lookup happens on every iteration; /// a callback may replace exec or recompile the receiver before the next one. /// Only the known builtin may omit materialization for a boolean test. +/// `reuse` is consulted only on the builtin path, after the observable lookup. pub(crate) fn execute( receiver: &RuntimeHandle<'_>, input: &RuntimeHandle<'_>, @@ -104,6 +105,7 @@ pub(crate) fn execute( budget: &mut Budget, memory: &MemoryBudget, poll: &mut impl FnMut() -> Result<(), EngineError>, + reuse: Option<&api::Reuse<'_, '_>>, ) -> Result, EngineError> { host::charge(budget, 1)?; require_object(receiver.get_nanbox_f64())?; @@ -137,7 +139,7 @@ pub(crate) fn execute( // `execute_with_resources` roots both before it allocates. input .with_const_ptr::(|input| { - api::execute_with_resources(re, input, materialize, budget, memory, poll) + api::execute_with_resources(re, input, materialize, budget, memory, poll, reuse) }) .map(|result| result.map(ExecResult::Builtin)) } @@ -318,6 +320,7 @@ pub(crate) fn test_string(receiver: f64, input: *const StringHeader) -> Result Result { let (global, unicode) = match_flags(receiver, budget)?; if !global { - return dispatch::execute(receiver, input, true, budget, memory, &mut host::poll) + return dispatch::execute(receiver, input, true, budget, memory, &mut host::poll, None) .map(|result| result.map_or(f64::from_bits(TAG_NULL), |r| r.object())); } dispatch::set_last_index(receiver, 0.0)?; let scope = RuntimeHandleScope::new(); let array = scope.root_raw_mut_ptr(api::caught(|| crate::array::js_array_alloc(0))?); let subject = subject(*input)?; + let reuse = api::Reuse::new(&scope, receiver, *input, &subject, budget); let length = input.with_const_ptr::(|s| unsafe { (*s).utf16_len as usize }); let mut count = 0u32; loop { // A fresh scope per iteration bounds roots regardless of match count. let iteration = RuntimeHandleScope::new(); - let result = dispatch::execute(receiver, input, false, budget, memory, &mut host::poll)?; + let result = dispatch::execute( + receiver, + input, + false, + budget, + memory, + &mut host::poll, + Some(&reuse), + )?; let Some(result) = result else { return Ok(if count == 0 { f64::from_bits(TAG_NULL) diff --git a/crates/perry-runtime/src/regex/perex_replace.rs b/crates/perry-runtime/src/regex/perex_replace.rs index cc07bcdf1f..303c6fa1c1 100644 --- a/crates/perry-runtime/src/regex/perex_replace.rs +++ b/crates/perry-runtime/src/regex/perex_replace.rs @@ -74,6 +74,7 @@ pub(crate) fn regexp(receiver: f64, argument: f64, replacement: f64) -> Result Result = const { std::cell::Cell::new(0) }; +} + +/// The program for split's forward search, when it is admissible (#10165). +/// +/// The specification tries a sticky match at every position `q`. 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, so the attempts +/// at `q..s` can be skipped without changing any piece or capture, and empty +/// matches and Unicode advancement line up. The skipped attempts are +/// unobservable only when nothing can see a RegExpExec happen: +/// - the splitter came from the intrinsic `RegExp` (absent or intrinsic +/// species), so it is a fresh object no user code holds, and its skipped +/// `lastIndex` writes cannot be seen; +/// - its `exec` resolves, without running a getter, to the builtin data +/// property, so the skipped `Get(exec)` calls cannot be seen either. +/// +/// The program is compiled from the splitter's own internal source and flags +/// without `y`. Anything else keeps the per-position sticky loop. +fn forward_program<'s>( + scope: &'s RuntimeHandleScope, + constructor: Option<&RuntimeHandle<'_>>, + splitter: &RuntimeHandle<'_>, + budget: &mut Budget, +) -> Option>> { + if constructor.is_some_and(|c| { + !crate::object::regex_proto_thunks::is_intrinsic_regexp_constructor(c.get_nanbox_f64()) + }) { + return None; + } + let value = splitter.get_nanbox_f64(); + let re = crate::value::js_nanbox_get_pointer(value) as *const super::RegExpHeader; + if !super::is_valid_regex_ptr(re) + || !crate::object::regex_proto_thunks::regexp_view_uses_builtin(value) + { + return None; + } + let splitter = scope.root_raw_const_ptr(re); + let program = super::perex_construct::nonsticky_program(scope, &splitter).ok()?; + BoundProgram::new(program, budget).ok() +} + fn push_span( output: &mut List<'_>, copies: &mut SpanCopies<'_, '_>, @@ -128,6 +174,7 @@ pub(crate) fn regexp(receiver: f64, argument: f64, limit_value: f64) -> Result Result= size { + break; + } + let end = found.full.end().min(size); + if end == p { + // Only an empty match at `p` itself: step past it, as the + // sticky loop does. + q = advance(&mut units, start, size, unicode, &mut budget)?; + host::poll()?; + continue; + } + push_span(&mut output, &mut copies, p, start, &mut budget)?; + if output.len() == lim { + return Ok(output.value()); + } + p = end; + let count = found.captures.as_ref().map_or(0, |captures| captures.len()); + if count > 1 { + let (array, _) = api::caught(|| { + super::perex_results::materialize( + &input, + &bound, + &forward, + &found, + false, + &mut budget, + &mut host::poll, + ) + })??; + let array = local.root_raw_mut_ptr(array); + for capture in 1..count { + let value = array.with_const_ptr::(|array| { + crate::array::js_array_get_f64(array, capture as u32) + }); + output.push(value, &mut budget)?; + if output.len() == lim { + return Ok(output.value()); + } + } + } + q = p; + host::poll()?; + } + push_span(&mut output, &mut copies, p, size, &mut budget)?; + return Ok(output.value()); + } while q < size { let local = RuntimeHandleScope::new(); dispatch::set_last_index(&splitter, q as f64)?; @@ -149,6 +263,7 @@ pub(crate) fn regexp(receiver: f64, argument: f64, limit_value: f64) -> Result `RegexMetadata { registered_owner: bool }`, so the VALUE holds no heap address at all (source and flags live only in the header's traced string edges, and the compiled program is a traced GC child of the header). The key is rekeyed by `regex_header_moved_for_gc` when a RegExpHeader moves and removed on death by the finalize hook, the copying-minor from-space walk and the full-cycle post-trace walk; it never keeps a header alive. Reached from those GC hooks rather than a registered scanner, so the walk misses it." }, + { + "file": "crates/perry-runtime/src/regex/perex_split.rs", + "name": "FORWARD_SPLITS", + "verdict": "test_only", + "why": "#10165: #[cfg(test)] Cell counter of how many splits took the forward-search path, so tests can tell which path ran. It stores only a count and is absent from shipped binaries." + }, { "file": "crates/perry-runtime/src/regex/site_test.rs", "name": "DIRECT_G",