diff --git a/nova_vm/src/ecmascript/builtins/text_processing/string_objects/string_prototype.rs b/nova_vm/src/ecmascript/builtins/text_processing/string_objects/string_prototype.rs index c3cd8056b..2c214ef66 100644 --- a/nova_vm/src/ecmascript/builtins/text_processing/string_objects/string_prototype.rs +++ b/nova_vm/src/ecmascript/builtins/text_processing/string_objects/string_prototype.rs @@ -10,24 +10,21 @@ use unicode_normalization::{ }; use wtf8::{CodePoint, Wtf8Buf}; +#[cfg(feature = "regexp")] +use crate::ecmascript::{Object, get_object_method, invoke, reg_exp_create}; use crate::{ ecmascript::{ Agent, ArgumentsList, Array, BUILTIN_STRING_MEMORY, Behaviour, Builtin, BuiltinIntrinsic, ExceptionType, JsResult, Number, Primitive, PrimitiveObjectData, PrimitiveObjectRecord, PropertyKey, Realm, String, StringIterator, Value, builders::OrdinaryObjectBuilder, - call_function, create_array_from_list, is_callable, is_reg_exp, is_trimmable_whitespace, - require_object_coercible, to_integer_or_infinity, to_integer_or_infinity_number, to_length, - to_number, to_string, to_string_primitive, to_uint32, try_result_into_js, - try_to_integer_or_infinity, try_to_length, try_to_string, + call_function, create_array_from_list, get, is_callable, is_reg_exp, + is_trimmable_whitespace, require_object_coercible, to_integer_or_infinity, + to_integer_or_infinity_number, to_length, to_number, to_string, to_string_primitive, + to_uint32, try_result_into_js, try_to_integer_or_infinity, try_to_length, try_to_string, }, - engine::{Bindable, GcScope, NoGcScope, Scopable}, + engine::{Bindable, GcScope, NoGcScope, Scopable, Scoped}, heap::{ArenaAccess, HeapIndexHandle, IntrinsicFunctionIndexes, WellKnownSymbols}, }; -#[cfg(feature = "regexp")] -use crate::{ - ecmascript::{Object, get, get_object_method, invoke, reg_exp_create}, - engine::Scoped, -}; pub(crate) struct StringPrototype; @@ -1754,13 +1751,47 @@ impl StringPrototype { // 6. If functionalReplace is false, Set replaceValue to ? ToString(replaceValue). let replace_string = to_string(agent, replace_value.get(agent), gc.reborrow()) .unbind()? - .bind(gc.nogc()); + .scope(agent, gc.nogc()); - // Everything are strings: `"foo".replace("o", "a")` => use rust's replace - let result = s.to_string_lossy(agent).into_owned().replacen( - search_string_root.to_string_lossy(agent).deref(), - &replace_string.to_string_lossy_(agent), - 1, + // 8. Let position be StringIndexOf(s, searchString, 0). + let subject = s.get(agent).to_string_lossy(agent).into_owned(); + let search_str = search_string_root + .get(agent) + .to_string_lossy(agent) + .into_owned(); + let utf8_position = match subject.find(search_str.deref()) { + Some(position) => position, + // 9. If position is not-found, return s. + None => return Ok(s.get(agent).into()), + }; + // 10. Let preceding be the substring of s from 0 to position. + let preceding = subject[..utf8_position].to_string(); + // 11. Let following be the substring of s from position + searchLength. + let following = subject[utf8_position + search_str.len()..].to_string(); + let position = s.get(agent).utf16_index_(agent, utf8_position); + + // 13. Else, + // a. Let captures be a new empty List. + // b. Let replacement be ? GetSubstitution(searchString, s, position, captures, undefined, replaceValue). + let replacement = get_substitution( + agent, + search_string_root.clone(), + s.clone(), + position, + vec![], + None, + replace_string, + gc.reborrow(), + ) + .unbind()? + .bind(gc.nogc()); + + // 14. Return the string-concatenation of preceding, replacement, and following. + let result = format!( + "{}{}{}", + preceding, + replacement.to_string_lossy_(agent), + following ); Ok(String::from_string(agent, result, gc.into_nogc()).into()) } @@ -1857,7 +1888,7 @@ impl StringPrototype { .scope(agent, gc.nogc()); // 4. Let searchString be ? ToString(searchValue). - let mut search_string = to_string(agent, scoped_search_value.get(agent), gc.reborrow()) + let search_string = to_string(agent, scoped_search_value.get(agent), gc.reborrow()) .unbind()? .bind(gc.nogc()); let search_string_root = search_string.scope(agent, gc.nogc()); @@ -1944,15 +1975,71 @@ impl StringPrototype { // 6. If functionalReplace is false, Set replaceValue to ? ToString(replaceValue). let replace_string = to_string(agent, replace_value.get(agent), gc.reborrow()) + .unbind()? + .scope(agent, gc.nogc()); + + // 7. Let searchLength be the length of searchString. + let search_str = search_string_root + .get(agent) + .to_string_lossy(agent) + .into_owned(); + let search_length = search_str.len(); + // 8. Let advanceBy be max(1, searchLength). + let advance_by = max(1, search_length); + + // 9-11. Collect all match positions (as UTF-8 byte offsets). + let subject = s.get(agent).to_string_lossy(agent).into_owned(); + let mut match_positions: Vec = vec![]; + let mut position = 0; + while let Some(pos) = subject + .split_at_checked(position) + .and_then(|(_, str)| str.find(search_str.deref())) + { + match_positions.push(position + pos); + position += advance_by + pos; + } + + // If none has found, return s. + if match_positions.is_empty() { + return Ok(s.get(agent).into()); + } + + // 12. Let endOfLastMatch be 0. + let mut end_of_last_match = 0; + // 13. Let result be the empty String. + let mut result = std::string::String::with_capacity(subject.len()); + // 14. For each element p of matchPositions, do + for p in match_positions { + // b. Else, let replacement be + // ? GetSubstitution(searchString, string, p, «», undefined, replaceValue). + let utf16_position = s.get(agent).utf16_index_(agent, p); + let replacement = get_substitution( + agent, + search_string_root.clone(), + s.clone(), + utf16_position, + vec![], + None, + replace_string.clone(), + gc.reborrow(), + ) .unbind()? .bind(gc.nogc()); - // Everything are strings: `"foo".replaceAll("o", "a")` => use rust's replace - search_string = search_string_root.get(agent).bind(gc.nogc()); - let s = s.get(agent).bind(gc.nogc()); - let result = s.to_string_lossy_(agent).into_owned().replace( - search_string.to_string_lossy_(agent).deref(), - &replace_string.to_string_lossy_(agent), - ); + // a. Let preserved be the substring of string from endOfLastMatch to p. + let preserved = &subject[end_of_last_match..p]; + // d. Set result to the string-concatenation of result, preserved, and replacement. + let replacement_str = replacement.to_string_lossy_(agent); + result.reserve(preserved.len() + replacement_str.len()); + result.push_str(preserved); + result.push_str(&replacement_str); + end_of_last_match = p + search_length; + } + + // 15. If endOfLastMatch < the length of string, append the remainder. + if end_of_last_match < subject.len() { + result.push_str(&subject[end_of_last_match..]); + } + // 16. Return result. Ok(String::from_string(agent, result, gc.into_nogc()).into()) } @@ -3337,7 +3424,6 @@ pub(crate) fn to_zero_padded_decimal_string( /// abstract operation, a decimal digit is a code unit in the inclusive /// interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE). #[allow(clippy::too_many_arguments)] -#[cfg(feature = "regexp")] pub(crate) fn get_substitution<'gc, 'scope>( agent: &mut Agent, scoped_matched: Scoped<'scope, String>, @@ -3376,13 +3462,15 @@ pub(crate) fn get_substitution<'gc, 'scope>( // a. NOTE: The following steps isolate ref (a prefix of // templateRemainder), determine refReplacement (its replacement), // and then append that replacement to result. - let mut r#ref = template_remainder; - let mut ref_replacement = std::borrow::Cow::Borrowed(template_remainder); - if template_remainder_bytes.len() == 1 { - // h. Else, - // i. Let ref be the substring of templateRemainder from 0 to 1. - // ii. Let refReplacement be ref. - } else if template_remainder_bytes[0] == b'$' { + // h. Else, let ref be the first code point of templateRemainder and + // let refReplacement be ref. + let first_char_len = template_remainder + .chars() + .next() + .map_or(1, |c| c.len_utf8()); + let mut r#ref = &template_remainder[..first_char_len]; + let mut ref_replacement = std::borrow::Cow::Borrowed(r#ref); + if template_remainder_bytes[0] == b'$' && template_remainder_bytes.len() > 1 { if template_remainder_bytes[1] == b'$' { // b. If templateRemainder starts with "$$", then // i. Let ref be "$$". @@ -3409,7 +3497,7 @@ pub(crate) fn get_substitution<'gc, 'scope>( // ii. Let matchLength be the length of matched. let match_length = matched.len(); // iii. Let tailPos be position + matchLength. - let tail_pos = position.saturating_add(match_length); + let tail_pos = utf8_position.saturating_add(match_length); // iv. Let refReplacement be the substring of str from // min(tailPos, stringLength). ref_replacement = str[tail_pos.min(utf8_string_length)..].into(); diff --git a/tests/expectations.json b/tests/expectations.json index 8fb7142c7..240b0405d 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -1184,7 +1184,6 @@ "built-ins/RegExp/S15.10.2.8_A3_T10.js": "FAIL", "built-ins/RegExp/S15.10.2.8_A3_T13.js": "FAIL", "built-ins/RegExp/S15.10.2.8_A3_T14.js": "FAIL", - "built-ins/RegExp/S15.10.2.8_A3_T18.js": "FAIL", "built-ins/RegExp/S15.10.2.8_A3_T7.js": "FAIL", "built-ins/RegExp/S15.10.2.8_A3_T8.js": "FAIL", "built-ins/RegExp/S15.10.2.8_A3_T9.js": "FAIL", @@ -1250,7 +1249,6 @@ "built-ins/RegExp/named-groups/non-unicode-property-names.js": "FAIL", "built-ins/RegExp/named-groups/non-unicode-references.js": "FAIL", "built-ins/RegExp/named-groups/string-replace-get.js": "FAIL", - "built-ins/RegExp/named-groups/string-replace-nocaptures.js": "FAIL", "built-ins/RegExp/named-groups/unicode-match.js": "FAIL", "built-ins/RegExp/named-groups/unicode-property-names-valid.js": "FAIL", "built-ins/RegExp/named-groups/unicode-property-names.js": "FAIL", @@ -1274,15 +1272,6 @@ "built-ins/RegExp/prototype/Symbol.replace/named-groups-fn.js": "FAIL", "built-ins/RegExp/prototype/Symbol.replace/named-groups.js": "FAIL", "built-ins/RegExp/prototype/Symbol.replace/poisoned-stdlib.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/result-coerce-capture.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/result-coerce-groups-prop.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/result-coerce-groups.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/result-coerce-matched.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/subst-after.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/subst-before.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/subst-capture-idx-1.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/subst-capture-idx-2.js": "FAIL", - "built-ins/RegExp/prototype/Symbol.replace/subst-matched.js": "FAIL", "built-ins/RegExp/prototype/Symbol.replace/u-advance-after-empty.js": "FAIL", "built-ins/RegExp/prototype/Symbol.replace/y-fail-lastindex-no-write.js": "FAIL", "built-ins/RegExp/prototype/Symbol.search/u-lastindex-advance.js": "FAIL", @@ -1676,14 +1665,8 @@ "built-ins/String/prototype/match/regexp-prototype-match-v-u-flag.js": "FAIL", "built-ins/String/prototype/matchAll/regexp-prototype-matchAll-v-u-flag.js": "FAIL", "built-ins/String/prototype/replace/S15.5.4.11_A5_T1.js": "FAIL", - "built-ins/String/prototype/replace/regexp-capture-by-index.js": "FAIL", "built-ins/String/prototype/replace/regexp-prototype-replace-v-u-flag.js": "CRASH", - "built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0024.js": "FAIL", - "built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0026.js": "FAIL", - "built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0027.js": "FAIL", "built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x003C.js": "FAIL", - "built-ins/String/prototype/replaceAll/getSubstitution-0x0024-0x0060.js": "FAIL", - "built-ins/String/prototype/replaceAll/getSubstitution-0x0024.js": "FAIL", "built-ins/String/prototype/replaceAll/getSubstitution-0x0024N.js": "FAIL", "built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js": "FAIL", "built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call-fn.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index e81399db9..39a5d80a9 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,8 +1,8 @@ { "results": { "crash": 52, - "fail": 6785, - "pass": 40515, + "fail": 6768, + "pass": 40532, "skip": 3326, "timeout": 18, "unresolved": 37