diff --git a/changelog.d/10207-regex-list-cap.md b/changelog.d/10207-regex-list-cap.md new file mode 100644 index 0000000000..7ecbeb28e4 --- /dev/null +++ b/changelog.d/10207-regex-list-cap.md @@ -0,0 +1,6 @@ +Removed the fixed entry cap on the regex runtime's operation lists, which made +`String.prototype.replace`, `replaceAll` and `split` throw "Regular expression +memory limit exceeded" once their output reached 8,388,608 list entries — a +callback `replace` over about 700,000 short records. Those lists are ordinary GC +arrays and are now bounded by allocation and the maximum string length, like +the rest of the runtime. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs index 3faa803d6a..b17ba58705 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_replace.rs @@ -492,3 +492,27 @@ fn perex_replace_primitive_search_skips_prototype_hook_and_coerces_receiver() { ); } } + +#[test] +fn perex_replace_output_is_not_capped_by_the_scratch_limit() { + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + // Each match after the first appends the preceding "b" and the "x" that + // replaces it: two pieces of three list entries each. The scratch limit + // divided by eight was the list's former entry cap, so this is one match + // past what could be written before. + let former_cap = api::SCRATCH_BYTES / 8; + let matches = former_cap / 6 + 1; + let input = text(&scope, &b"ab".repeat(matches)); + let search = text(&scope, b"a"); + let replacement = text(&scope, b"x"); + let result = crate::regex::js_string_replace_all_js( + input.get_nanbox_f64(), + search.get_nanbox_f64(), + replacement.get_nanbox_f64(), + ); + let output = bytes(result); + assert_eq!(output.len(), matches * 2); + assert!(output.as_chunks::<2>().0.iter().all(|pair| pair == b"xb")); +} diff --git a/crates/perry-runtime/src/regex/perex_replace_storage.rs b/crates/perry-runtime/src/regex/perex_replace_storage.rs index d0c752f62f..95f5d2ee42 100644 --- a/crates/perry-runtime/src/regex/perex_replace_storage.rs +++ b/crates/perry-runtime/src/regex/perex_replace_storage.rs @@ -52,11 +52,13 @@ impl<'a> List<'a> { self.root .with_const_ptr(|array| crate::array::js_array_get_f64(array, index as u32)) } + /// Append one value. The list is an ordinary GC array, so it is bounded + /// by what can be allocated, not by a count: the scratch limit is for + /// native buffers, and a list's length follows the subject. A replacement + /// producing more pieces than that limit's entries once threw a memory + /// error on subjects Node replaces in a fraction of a second (#10164). pub(super) fn push(&mut self, value: f64, budget: &mut Budget) -> Result<(), EngineError> { host::charge(budget, 1)?; - if self.count >= api::SCRATCH_BYTES / 8 { - return Err(StorageError::Limit.into()); - } let scope = RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(value); let array = api::caught(|| {