author | Tooru Fujisawa <arai_a@mac.com> |
Sat, 05 Sep 2015 21:55:06 +0900 | |
changeset 292175 | b5b06959919ad3a0150c7ca1dfe0de7d2d9df7e1 |
parent 292174 | ac74c5fe92172d8dbb363eb713e1512570c51af1 |
child 292176 | 3a37c4b0e33804f0af4e3d05fdcd087e6b359f00 |
push id | 74764 |
push user | arai_a@mac.com |
push date | Thu, 07 Apr 2016 10:49:15 +0000 |
treeherder | mozilla-inbound@4d0f975a2311 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | till |
bugs | 887016 |
milestone | 48.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/js/src/builtin/RegExp.cpp +++ b/js/src/builtin/RegExp.cpp @@ -639,16 +639,17 @@ const JSPropertySpec js::regexp_properti const JSFunctionSpec js::regexp_methods[] = { #if JS_HAS_TOSOURCE JS_SELF_HOSTED_FN(js_toSource_str, "RegExpToString", 0, 0), #endif JS_SELF_HOSTED_FN(js_toString_str, "RegExpToString", 0, 0), JS_FN("compile", regexp_compile, 2,0), JS_SELF_HOSTED_FN("exec", "RegExp_prototype_Exec", 1,0), JS_SELF_HOSTED_FN("test", "RegExpTest" , 1,0), + JS_SELF_HOSTED_SYM_FN(match, "RegExpMatch", 1,0), JS_FS_END }; #define STATIC_PAREN_GETTER_CODE(parenNum) \ if (!res->createParen(cx, parenNum, args.rval())) \ return false; \ if (args.rval().isUndefined()) \ args.rval().setString(cx->runtime()->emptyString); \
--- a/js/src/builtin/RegExp.js +++ b/js/src/builtin/RegExp.js @@ -51,16 +51,107 @@ function RegExpToString() // Steps 5-6. var flags = R.flags; // Step 7. return '/' + pattern + '/' + flags; } _SetCanonicalName(RegExpToString, "toString"); +// ES 2016 draft Mar 25, 2016 21.2.5.2.3. +function AdvanceStringIndex(S, index) { + // Step 1. + assert(typeof S === "string", "Expected string as 1st argument"); + + // Step 2. + assert(index >= 0 && index <= MAX_NUMERIC_INDEX, "Expected integer as 2nd argument"); + + // Step 3 (skipped). + + // Step 4 (skipped). + + // Step 5. + var length = S.length; + + // Step 6. + if (index + 1 >= length) + return index + 1; + + // Step 7. + var first = callFunction(std_String_charCodeAt, S, index); + + // Step 8. + if (first < 0xD800 || first > 0xDBFF) + return index + 1; + + // Step 9. + var second = callFunction(std_String_charCodeAt, S, index + 1); + + // Step 10. + if (second < 0xDC00 || second > 0xDFFF) + return index + 1; + + // Step 11. + return index + 2; +} + +// ES 2016 draft Mar 25, 2016 21.2.5.6. +function RegExpMatch(string) { + // Step 1. + var rx = this; + + // Step 2. + if (!IsObject(rx)) + ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, rx === null ? "null" : typeof rx); + + // Step 3. + var S = ToString(string); + + // Steps 4-5. + if (!rx.global) + return RegExpExec(rx, S, false); + + // Step 6.a. + var fullUnicode = !!rx.unicode; + + // Step 6.b. + rx.lastIndex = 0; + + // Step 6.c. + var A = []; + + // Step 6.d. + var n = 0; + + // Step 6.e. + while (true) { + // Step 6.e.i. + var result = RegExpExec(rx, S, false); + + // Step 6.e.ii. + if (result === null) + return (n === 0) ? null : A; + + // Step 6.e.iii.1. + var matchStr = ToString(result[0]); + + // Step 6.e.iii.2. + _DefineDataProperty(A, n, matchStr); + + // Step 6.e.iii.4. + if (matchStr === "") { + var lastIndex = ToLength(rx.lastIndex); + rx.lastIndex = fullUnicode ? AdvanceStringIndex(S, lastIndex) : lastIndex + 1; + } + + // Step 6.e.iii.5. + n++; + } +} + // ES6 21.2.5.2. // NOTE: This is not RegExpExec (21.2.5.2.1). function RegExp_prototype_Exec(string) { // Steps 1-3. var R = this; if (!IsObject(R) || !IsRegExpObject(R)) return callFunction(CallRegExpMethodIfWrapped, R, string, "RegExp_prototype_Exec");
--- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -1,14 +1,81 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global intl_Collator: false, */ +function StringProtoHasNoMatch() { + var ObjectProto = GetBuiltinPrototype("Object"); + var StringProto = GetBuiltinPrototype("String"); + if (!ObjectHasPrototype(StringProto, ObjectProto)) + return false; + return !(std_match in StringProto); +} + +function IsStringMatchOptimizable() { + var RegExpProto = GetBuiltinPrototype("RegExp"); + // If RegExpPrototypeOptimizable succeeds, `exec` and `@@match` are + // guaranteed to be data properties. + return RegExpPrototypeOptimizable(RegExpProto) && + RegExpProto.exec === RegExp_prototype_Exec && + RegExpProto[std_match] === RegExpMatch; +} + +// ES 2016 draft Mar 25, 2016 21.1.3.11. +function String_match(regexp) { + // Step 1. + RequireObjectCoercible(this); + + // Step 2. + var isPatternString = (typeof regexp === "string"); + if (!(isPatternString && StringProtoHasNoMatch()) && regexp !== undefined && regexp !== null) { + // Step 2.a. + var matcher = GetMethod(regexp, std_match); + + // Step 2.b. + if (matcher !== undefined) + return callContentFunction(matcher, regexp, this); + } + + // Step 3. + var S = ToString(this); + + // FIXME: Non-standard flags argument (bug 1108382). + var flags = undefined; + if (arguments.length > 1) { + if (IsMatchFlagsArgumentEnabled()) + flags = arguments[1]; + WarnOnceAboutFlagsArgument(); + } else { + if (isPatternString && IsStringMatchOptimizable()) { + var flatResult = FlatStringMatch(S, regexp); + if (flatResult !== undefined) + return flatResult; + } + } + + // Step 4. + var rx = RegExpCreate(regexp, flags); + + // Step 5 (optimized case). + if (IsStringMatchOptimizable() && !flags) + return RegExpMatcher(rx, S, 0, false); + + // Step 5. + return callContentFunction(GetMethod(rx, std_match), rx, S); +} + +function String_generic_match(thisValue, regexp) { + if (thisValue === undefined) + ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.match'); + return callFunction(String_match, thisValue, regexp); +} + /** * A helper function implementing the logic for both String.prototype.padStart * and String.prototype.padEnd as described in ES7 Draft March 29, 2016 */ function String_pad(maxLength, fillString, padEnd=false) { // Steps 1-2. RequireObjectCoercible(this);
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/ion/testStringMatch.js @@ -0,0 +1,119 @@ +setJitCompilerOption("ion.warmup.trigger", 4); + +function testBasic() { + var f = function() { + var result = "abc".match("b"); + assertEq(result.length, 1); + assertEq(result.index, 1); + assertEq(result[0], "b"); + }; + for (var i = 0; i < 40; i++) { + f(); + } +} +testBasic(); + +function testMod(apply, unapply) { + var f = function(applied) { + var result = "abc".match("b"); + assertEq(result.length, 1); + if (applied) { + assertEq(result[0], "mod"); + } else { + assertEq(result.index, 1); + assertEq(result[0], "b"); + } + }; + var applied = false; + for (var i = 0; i < 120; i++) { + f(applied); + if (i == 40) { + apply(); + applied = true; + } + if (i == 80) { + unapply(); + applied = false; + } + } +} +testMod(() => { + String.prototype[Symbol.match] = () => ["mod"]; +}, () => { + delete String.prototype[Symbol.match]; +}); +testMod(() => { + Object.prototype[Symbol.match] = () => ["mod"]; +}, () => { + delete Object.prototype[Symbol.match]; +}); + +testMod(() => { + Object.setPrototypeOf(String.prototype, { + [Symbol.match]: () => ["mod"] + }); +}, () => { + Object.setPrototypeOf(String.prototype, Object.prototype); +}); + +var orig_exec = RegExp.prototype.exec; +testMod(() => { + RegExp.prototype.exec = () => ["mod"]; +}, () => { + RegExp.prototype.exec = orig_exec; +}); + +var orig_match = RegExp.prototype[Symbol.match]; +testMod(() => { + RegExp.prototype[Symbol.match] = () => ["mod"]; +}, () => { + RegExp.prototype[Symbol.match] = orig_match; +}); + +var observed = false; +function testObserved(apply, unapply) { + var f = function(applied) { + observed = false; + var result = "abc".match("b."); // Use meta char to avoid flat match. + assertEq(result.length, 1); + assertEq(result.index, 1); + assertEq(result[0], "bc"); + assertEq(observed, applied); + }; + var applied = false; + for (var i = 0; i < 120; i++) { + f(applied); + if (i == 40) { + apply(); + applied = true; + } + if (i == 80) { + unapply(); + applied = false; + } + } +} + +var orig_global = Object.getOwnPropertyDescriptor(RegExp.prototype, "global"); +testObserved(() => { + Object.defineProperty(RegExp.prototype, "global", { + get: function() { + observed = true; + return false; + } + }); +}, () => { + Object.defineProperty(RegExp.prototype, "global", orig_global); +}); + +var orig_sticky = Object.getOwnPropertyDescriptor(RegExp.prototype, "sticky"); +testObserved(() => { + Object.defineProperty(RegExp.prototype, "sticky", { + get: function() { + observed = true; + return false; + } + }); +}, () => { + Object.defineProperty(RegExp.prototype, "sticky", orig_sticky); +});
--- a/js/src/jit/Lowering.cpp +++ b/js/src/jit/Lowering.cpp @@ -2185,17 +2185,16 @@ MustCloneRegExpForCall(MCall* call, uint JSFunction* target = call->getSingleTarget(); if (!target || !target->isNative()) return true; if (useIndex == MCall::IndexOfArgument(0) && (target->native() == str_split || target->native() == str_replace || - target->native() == str_match || target->native() == str_search)) { return false; } return true; }
--- a/js/src/js.msg +++ b/js/src/js.msg @@ -439,16 +439,17 @@ MSG_DEF(JSMSG_INVALID_LOCALE_MATCHER, 1 MSG_DEF(JSMSG_INVALID_OPTION_VALUE, 2, JSEXN_RANGEERR, "invalid value {1} for option {0}") MSG_DEF(JSMSG_INVALID_TIME_ZONE, 1, JSEXN_RANGEERR, "invalid time zone in DateTimeFormat(): {0}") MSG_DEF(JSMSG_UNDEFINED_CURRENCY, 0, JSEXN_TYPEERR, "undefined currency in NumberFormat() with currency style") // RegExp MSG_DEF(JSMSG_BACK_REF_OUT_OF_RANGE, 0, JSEXN_SYNTAXERR, "back reference out of range in regular expression") MSG_DEF(JSMSG_BAD_CLASS_RANGE, 0, JSEXN_SYNTAXERR, "invalid range in character class") MSG_DEF(JSMSG_ESCAPE_AT_END_OF_REGEXP, 0, JSEXN_SYNTAXERR, "\\ at end of pattern") +MSG_DEF(JSMSG_EXEC_NOT_OBJORNULL, 0, JSEXN_TYPEERR, "RegExp exec method should return object or null") MSG_DEF(JSMSG_INVALID_DECIMAL_ESCAPE, 0, JSEXN_SYNTAXERR, "invalid decimal escape in regular expression") MSG_DEF(JSMSG_INVALID_GROUP, 0, JSEXN_SYNTAXERR, "invalid regexp group") MSG_DEF(JSMSG_INVALID_IDENTITY_ESCAPE, 0, JSEXN_SYNTAXERR, "invalid identity escape in regular expression") MSG_DEF(JSMSG_INVALID_UNICODE_ESCAPE, 0, JSEXN_SYNTAXERR, "invalid unicode escape in regular expression") MSG_DEF(JSMSG_MISSING_PAREN, 0, JSEXN_SYNTAXERR, "unterminated parenthetical") MSG_DEF(JSMSG_NEWREGEXP_FLAGGED, 0, JSEXN_TYPEERR, "can't supply flags when constructing one RegExp from another") MSG_DEF(JSMSG_NOTHING_TO_REPEAT, 0, JSEXN_SYNTAXERR, "nothing to repeat") MSG_DEF(JSMSG_NUMBERS_OUT_OF_ORDER, 0, JSEXN_SYNTAXERR, "numbers out of order in {} quantifier.")
--- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -2255,42 +2255,16 @@ class MOZ_STACK_CLASS StringRegExpGuard private: StringRegExpGuard(const StringRegExpGuard&) = delete; void operator=(const StringRegExpGuard&) = delete; }; } /* anonymous namespace */ -static bool -DoMatchLocal(JSContext* cx, const CallArgs& args, RegExpStatics* res, HandleLinearString input, - RegExpShared& re) -{ - ScopedMatchPairs matches(&cx->tempLifoAlloc()); - bool sticky = re.sticky(); - RegExpRunStatus status = re.execute(cx, input, 0, sticky, &matches, nullptr); - if (status == RegExpRunStatus_Error) - return false; - - if (status == RegExpRunStatus_Success_NotFound) { - args.rval().setNull(); - return true; - } - - if (!res->updateFromMatchPairs(cx, input, matches)) - return false; - - RootedValue rval(cx); - if (!CreateRegExpMatchResult(cx, input, matches, &rval)) - return false; - - args.rval().set(rval); - return true; -} - /* ES6 21.2.5.2.3. */ static size_t AdvanceStringIndex(HandleLinearString input, size_t length, size_t index, bool unicode) { /* Steps 1-3 (implicit). */ /* Step 4: If input is latin1, there is no surrogate pair. */ if (!unicode || input->hasLatin1Chars()) @@ -2316,209 +2290,16 @@ AdvanceStringIndex(HandleLinearString in /* Step 10. */ if (!unicode::IsTrailSurrogate(second)) return index + 1; /* Step 11. */ return index + 2; } -/* ES5 15.5.4.10 step 8. */ -static bool -DoMatchGlobal(JSContext* cx, const CallArgs& args, RegExpStatics* res, HandleLinearString input, - StringRegExpGuard& g) -{ - // Step 8a. - // - // This single zeroing of "lastIndex" covers all "lastIndex" changes in the - // rest of String.prototype.match, particularly in steps 8f(i) and - // 8f(iii)(2)(a). Here's why. - // - // The inputs to the calls to RegExp.prototype.exec are a RegExp object - // whose .global is true and a string. The only side effect of a call in - // these circumstances is that the RegExp's .lastIndex will be modified to - // the next starting index after the discovered match (or to 0 if there's - // no remaining match). Because .lastIndex is a non-configurable data - // property and no script-controllable code executes after step 8a, passing - // step 8a implies *every* .lastIndex set succeeds. String.prototype.match - // calls RegExp.prototype.exec repeatedly, and the last call doesn't match, - // so the final value of .lastIndex is 0: exactly the state after step 8a - // succeeds. No spec step lets script observe intermediate .lastIndex - // values. - // - // The arrays returned by RegExp.prototype.exec always have a string at - // index 0, for which [[Get]]s have no side effects. - // - // Filling in a new array using [[DefineOwnProperty]] is unobservable. - // - // This is a tricky point, because after this set, our implementation *can* - // fail. The key is that script can't distinguish these failure modes from - // one where, in spec terms, we fail immediately after step 8a. That *in - // reality* we might have done extra matching work, or created a partial - // results array to return, or hit an interrupt, is irrelevant. The - // script can't tell we did any of those things but didn't update - // .lastIndex. Thus we can optimize steps 8b onward however we want, - // including eliminating intermediate .lastIndex sets, as long as we don't - // add ways for script to observe the intermediate states. - // - // In short: it's okay to cheat (by setting .lastIndex to 0, once) because - // we can't get caught. - if (!g.zeroLastIndex(cx)) - return false; - - // Step 8b. - AutoValueVector elements(cx); - - size_t lastSuccessfulStart = 0; - - // The loop variables from steps 8c-e aren't needed, as we use different - // techniques from the spec to implement step 8f's loop. - - // Step 8f. - ScopedMatchPairs matches(&cx->tempLifoAlloc()); - size_t charsLen = input->length(); - RegExpShared& re = g.regExp(); - bool unicode = re.unicode(); - bool sticky = re.sticky(); - for (size_t searchIndex = 0; searchIndex <= charsLen; ) { - if (!CheckForInterrupt(cx)) - return false; - - // Steps 8f(i-ii), minus "lastIndex" updates (see above). - RegExpRunStatus status = re.execute(cx, input, searchIndex, sticky, &matches, nullptr); - if (status == RegExpRunStatus_Error) - return false; - - // Step 8f(ii). - if (status == RegExpRunStatus_Success_NotFound) - break; - - lastSuccessfulStart = searchIndex; - MatchPair& match = matches[0]; - - // Steps 8f(iii)(1-3). - searchIndex = match.isEmpty() - ? AdvanceStringIndex(input, charsLen, match.limit, unicode) - : match.limit; - - // Step 8f(iii)(4-5). - JSLinearString* str = NewDependentString(cx, input, match.start, match.length()); - if (!str) - return false; - if (!elements.append(StringValue(str))) - return false; - } - - // Step 8g. - if (elements.empty()) { - args.rval().setNull(); - return true; - } - - // The last *successful* match updates the RegExpStatics. (Interestingly, - // this implies that String.prototype.match's semantics aren't those - // implied by the RegExp.prototype.exec calls in the ES5 algorithm.) - res->updateLazily(cx, input, &re, lastSuccessfulStart, sticky); - - // Steps 8b, 8f(iii)(5-6), 8h. - JSObject* array = NewDenseCopiedArray(cx, elements.length(), elements.begin()); - if (!array) - return false; - - args.rval().setObject(*array); - return true; -} - -static bool -BuildFlatMatchArray(JSContext* cx, HandleString textstr, const FlatMatch& fm, CallArgs* args) -{ - if (fm.match() < 0) { - args->rval().setNull(); - return true; - } - - /* Get the templateObject that defines the shape and type of the output object */ - JSObject* templateObject = cx->compartment()->regExps.getOrCreateMatchResultTemplateObject(cx); - if (!templateObject) - return false; - - RootedArrayObject arr(cx, NewDenseFullyAllocatedArrayWithTemplate(cx, 1, templateObject)); - if (!arr) - return false; - - /* Store a Value for each pair. */ - arr->setDenseInitializedLength(1); - arr->initDenseElement(0, StringValue(fm.pattern())); - - /* Set the |index| property. (TemplateObject positions it in slot 0) */ - arr->setSlot(0, Int32Value(fm.match())); - - /* Set the |input| property. (TemplateObject positions it in slot 1) */ - arr->setSlot(1, StringValue(textstr)); - -#ifdef DEBUG - RootedValue test(cx); - RootedId id(cx, NameToId(cx->names().index)); - if (!NativeGetProperty(cx, arr, id, &test)) - return false; - MOZ_ASSERT(test == arr->getSlot(0)); - id = NameToId(cx->names().input); - if (!NativeGetProperty(cx, arr, id, &test)) - return false; - MOZ_ASSERT(test == arr->getSlot(1)); -#endif - - args->rval().setObject(*arr); - return true; -} - -/* ES5 15.5.4.10. */ -bool -js::str_match(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - /* Steps 1-2. */ - RootedString str(cx, ThisToStringForStringProto(cx, args)); - if (!str) - return false; - - /* Steps 3-4, plus the trailing-argument "flags" extension. */ - StringRegExpGuard g(cx); - if (!g.init(cx, args, true)) - return false; - - /* Fast path when the search pattern can be searched for as a string. */ - if (const FlatMatch* fm = g.tryFlatMatch(cx, str, 1, args.length())) - return BuildFlatMatchArray(cx, str, *fm, &args); - - /* Return if there was an error in tryFlatMatch. */ - if (cx->isExceptionPending()) - return false; - - /* Create regular-expression internals as needed to perform the match. */ - if (!g.normalizeRegExp(cx, false, 1, args)) - return false; - - RegExpStatics* res = cx->global()->getRegExpStatics(cx); - if (!res) - return false; - - RootedLinearString linearStr(cx, str->ensureLinear(cx)); - if (!linearStr) - return false; - - /* Steps 5-6, 7. */ - if (!g.regExp().global()) - return DoMatchLocal(cx, args, res, linearStr, g.regExp()); - - /* Steps 6, 8. */ - return DoMatchGlobal(cx, args, res, linearStr, g); -} - bool js::str_search(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); RootedString str(cx, ThisToStringForStringProto(cx, args)); if (!str) return false; @@ -4208,17 +3989,17 @@ static const JSFunctionSpec string_metho JS_FN("localeCompare", str_localeCompare, 1,JSFUN_GENERIC_NATIVE), #endif JS_SELF_HOSTED_FN("repeat", "String_repeat", 1,0), #if EXPOSE_INTL_API JS_FN("normalize", str_normalize, 0,JSFUN_GENERIC_NATIVE), #endif /* Perl-ish methods (search is actually Python-esque). */ - JS_FN("match", str_match, 1,JSFUN_GENERIC_NATIVE), + JS_SELF_HOSTED_FN("match", "String_match", 1,0), JS_FN("search", str_search, 1,JSFUN_GENERIC_NATIVE), JS_INLINABLE_FN("replace", str_replace, 2,JSFUN_GENERIC_NATIVE, StringReplace), JS_INLINABLE_FN("split", str_split, 2,JSFUN_GENERIC_NATIVE, StringSplit), JS_SELF_HOSTED_FN("substr", "String_substr", 2,0), /* Python-esque sequence methods. */ JS_FN("concat", str_concat, 1,JSFUN_GENERIC_NATIVE), JS_SELF_HOSTED_FN("slice", "String_slice", 2,0), @@ -4363,16 +4144,18 @@ static const JSFunctionSpec string_stati JS_INLINABLE_FN("fromCharCode", js::str_fromCharCode, 1, 0, StringFromCharCode), JS_SELF_HOSTED_FN("fromCodePoint", "String_static_fromCodePoint", 1,0), JS_SELF_HOSTED_FN("raw", "String_static_raw", 2,JSFUN_HAS_REST), JS_SELF_HOSTED_FN("substring", "String_static_substring", 3,0), JS_SELF_HOSTED_FN("substr", "String_static_substr", 3,0), JS_SELF_HOSTED_FN("slice", "String_static_slice", 3,0), + JS_SELF_HOSTED_FN("match", "String_generic_match", 2,0), + // This must be at the end because of bug 853075: functions listed after // self-hosted methods aren't available in self-hosted code. #if EXPOSE_INTL_API JS_SELF_HOSTED_FN("localeCompare", "String_static_localeCompare", 2,0), #endif JS_FS_END }; @@ -5394,8 +5177,132 @@ js::PutEscapedStringImpl(char* buffer, s template size_t js::PutEscapedString(char* buffer, size_t bufferSize, const Latin1Char* chars, size_t length, uint32_t quote); template size_t js::PutEscapedString(char* buffer, size_t bufferSize, const char16_t* chars, size_t length, uint32_t quote); + +static bool +FlatStringMatchHelper(JSContext* cx, HandleString str, HandleString pattern, bool* isFlat, int32_t* match) +{ + RootedLinearString linearPattern(cx, pattern->ensureLinear(cx)); + if (!linearPattern) + return false; + + static const size_t MAX_FLAT_PAT_LEN = 256; + if (linearPattern->length() > MAX_FLAT_PAT_LEN || StringHasRegExpMetaChars(linearPattern)) { + *isFlat = false; + return true; + } + + *isFlat = true; + if (str->isRope()) { + if (!RopeMatch(cx, &str->asRope(), linearPattern, match)) + return false; + } else { + *match = StringMatch(&str->asLinear(), linearPattern); + } + + return true; +} + +static bool +BuildFlatMatchArray(JSContext* cx, HandleString str, HandleString pattern, int32_t match, + MutableHandleValue rval) +{ + if (match < 0) { + rval.setNull(); + return true; + } + + /* Get the templateObject that defines the shape and type of the output object */ + JSObject* templateObject = cx->compartment()->regExps.getOrCreateMatchResultTemplateObject(cx); + if (!templateObject) + return false; + + RootedArrayObject arr(cx, NewDenseFullyAllocatedArrayWithTemplate(cx, 1, templateObject)); + if (!arr) + return false; + + /* Store a Value for each pair. */ + arr->setDenseInitializedLength(1); + arr->initDenseElement(0, StringValue(pattern)); + + /* Set the |index| property. (TemplateObject positions it in slot 0) */ + arr->setSlot(0, Int32Value(match)); + + /* Set the |input| property. (TemplateObject positions it in slot 1) */ + arr->setSlot(1, StringValue(str)); + +#ifdef DEBUG + RootedValue test(cx); + RootedId id(cx, NameToId(cx->names().index)); + if (!NativeGetProperty(cx, arr, id, &test)) + return false; + MOZ_ASSERT(test == arr->getSlot(0)); + id = NameToId(cx->names().input); + if (!NativeGetProperty(cx, arr, id, &test)) + return false; + MOZ_ASSERT(test == arr->getSlot(1)); +#endif + + rval.setObject(*arr); + return true; +} + +#ifdef DEBUG +static bool +CallIsStringOptimizable(JSContext* cx, const char* name, bool* result) +{ + JSAtom* atom = Atomize(cx, name, strlen(name)); + if (!atom) + return false; + RootedPropertyName propName(cx, atom->asPropertyName()); + + RootedValue funcVal(cx); + if (!GlobalObject::getSelfHostedFunction(cx, cx->global(), propName, propName, 0, &funcVal)) + return false; + + InvokeArgs args(cx); + if (!args.init(0)) + return false; + args.setCallee(funcVal); + args.setThis(UndefinedValue()); + if (!Invoke(cx, args)) + return false; + + *result = args.rval().toBoolean(); + return true; +} +#endif + +bool +js::FlatStringMatch(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); +#ifdef DEBUG + bool isOptimizable = false; + if (!CallIsStringOptimizable(cx, "IsStringMatchOptimizable", &isOptimizable)) + return false; + MOZ_ASSERT(isOptimizable); +#endif + + RootedString str(cx,args[0].toString()); + RootedString pattern(cx, args[1].toString()); + + bool isFlat = false; + int32_t match = 0; + if (!FlatStringMatchHelper(cx, str, pattern, &isFlat, &match)) + return false; + + if (!isFlat) { + args.rval().setUndefined(); + return true; + } + + return BuildFlatMatchArray(cx, str, pattern, match, args.rval()); +}
--- a/js/src/jsstr.h +++ b/js/src/jsstr.h @@ -426,19 +426,16 @@ FileEscapedString(FILE* fp, const char* { Fprinter out(fp); bool res = EscapedStringPrinter(out, chars, length, quote); out.finish(); return res; } bool -str_match(JSContext* cx, unsigned argc, Value* vp); - -bool str_search(JSContext* cx, unsigned argc, Value* vp); bool str_split(JSContext* cx, unsigned argc, Value* vp); JSObject* str_split_string(JSContext* cx, HandleObjectGroup group, HandleString str, HandleString sep); @@ -448,11 +445,14 @@ str_flat_replace_string(JSContext *cx, H JSString* str_replace_string_raw(JSContext* cx, HandleString string, HandleString pattern, HandleString replacement); extern bool StringConstructor(JSContext* cx, unsigned argc, Value* vp); +extern bool +FlatStringMatch(JSContext* cx, unsigned argc, Value* vp); + } /* namespace js */ #endif /* jsstr_h */
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/RegExp/RegExpExec-exec.js @@ -0,0 +1,18 @@ +var BUGNUMBER = 887016; +var summary = "RegExpExec should throw if exec property of non-RegExp is not callable"; + +print(BUGNUMBER + ": " + summary); + +for (var exec of [null, 0, false, undefined, ""]) { + // RegExp with non-callable exec + var re = /a/; + re.exec = exec; + RegExp.prototype[Symbol.match].call(re, "foo"); + + // non-RegExp with non-callable exec + assertThrowsInstanceOf(() => RegExp.prototype[Symbol.match].call({ exec }, "foo"), + TypeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true);
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/RegExp/RegExpExec-return.js @@ -0,0 +1,31 @@ +var BUGNUMBER = 887016; +var summary = "RegExpExec should throw if returned value is not an object nor null."; + +print(BUGNUMBER + ": " + summary); + +for (var ret of [null, {}, [], /a/]) { + assertEq(RegExp.prototype[Symbol.match].call({ + get global() { + return false; + }, + exec(S) { + return ret; + } + }, "foo"), ret); +} + +for (ret of [undefined, 1, true, false, Symbol.iterator]) { + assertThrowsInstanceOf(() => { + RegExp.prototype[Symbol.match].call({ + get global() { + return false; + }, + exec(S) { + return ret; + } + }, "foo"); + }, TypeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true);
--- a/js/src/tests/ecma_6/RegExp/constructor-constructor.js +++ b/js/src/tests/ecma_6/RegExp/constructor-constructor.js @@ -20,16 +20,34 @@ assertEq(RegExp(re) === re, false); assertEq(RegExp(re).toString(), re.toString()); re = new Proxy(/a/, { get(that, name) { return that[name]; } }); +assertEq(RegExp(re), re); +re = new Proxy(/a/, { + get(that, name) { + if (name == "constructor") { + return function() {}; + } + return that[name]; + } +}); +assertEq(RegExp(re) === re, false); +re = new Proxy(/a/, { + get(that, name) { + if (name == Symbol.match) { + return undefined; + } + return that[name]; + } +}); assertEq(RegExp(re) === re, false); re = new Proxy(g.eval(`/a/`), { get(that, name) { return that[name]; } }); assertEq(RegExp(re) === re, false);
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/RegExp/match-this.js @@ -0,0 +1,12 @@ +var BUGNUMBER = 887016; +var summary = "RegExp.prototype[@@match] should check this value."; + +print(BUGNUMBER + ": " + summary); + +for (var v of [null, 1, true, undefined, "", Symbol.iterator]) { + assertThrowsInstanceOf(() => RegExp.prototype[Symbol.match].call(v), + TypeError); +} + +if (typeof reportCompare === "function") + reportCompare(true, true);
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/RegExp/match-trace.js @@ -0,0 +1,147 @@ +var BUGNUMBER = 887016; +var summary = "Trace RegExp.prototype[@@match] behavior."; + +print(BUGNUMBER + ": " + summary); + +var n; +var log; +var target; +var global; +var unicode; +var logProxy; + +var execResult; +var lastIndexResult; +var lastIndexExpected; + +function P(A) { + return new Proxy(A, { + get(that, name) { + if (logProxy) + log += "get:result[" + name + "],"; + return that[name]; + } + }); +} + +var myRegExp = { + get global() { + log += "get:global,"; + return global; + }, + get lastIndex() { + log += "get:lastIndex,"; + return lastIndexResult[n]; + }, + set lastIndex(v) { + log += "set:lastIndex,"; + assertEq(v, lastIndexExpected[n]); + }, + get unicode() { + log += "get:unicode,"; + return unicode; + }, + get exec() { + log += "get:exec,"; + return function(S) { + log += "call:exec,"; + assertEq(S, target); + return execResult[n++]; + }; + }, +}; + +function reset() { + n = 0; + log = ""; + target = "abcAbcABC"; + global = true; + unicode = false; + logProxy = true; +} + +// Trace global with non-empty match. +reset(); +execResult = [ P(["abc"]), P(["ABC"]), null ]; +lastIndexResult = [ , , , ]; +lastIndexExpected = [ 0, , , ]; +var ret = RegExp.prototype[Symbol.match].call(myRegExp, target); +assertEq(JSON.stringify(ret), `["abc","ABC"]`); +assertEq(log, + "get:global," + + "get:unicode," + + "set:lastIndex," + + "get:exec,call:exec,get:result[0]," + + "get:exec,call:exec,get:result[0]," + + "get:exec,call:exec,"); + +// Trace global with empty match. +reset(); +execResult = [ P([""]), P([""]), null ]; +lastIndexResult = [ , 4, 20, ]; +lastIndexExpected = [ 0, 5, 21, ]; +ret = RegExp.prototype[Symbol.match].call(myRegExp, target); +assertEq(JSON.stringify(ret), `["",""]`); +assertEq(log, + "get:global," + + "get:unicode," + + "set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,"); + +// Trace global and unicode with empty match. +// 1. not surrogate pair +// 2. lead surrogate pair +// 3. trail surrogate pair +// 4. lead surrogate pair without trail surrogate pair +// 5. index overflow +reset(); +unicode = true; +// 0123 4 5678 +target = "___\uD83D\uDC38___\uD83D"; +execResult = [ P([""]), P([""]), P([""]), P([""]), P([""]), null ]; +lastIndexResult = [ , 2, 3, 4, 8, 9, ]; +lastIndexExpected = [ 0, 3, 5, 5, 9, 10, ]; +ret = RegExp.prototype[Symbol.match].call(myRegExp, target); +assertEq(JSON.stringify(ret), `["","","","",""]`); +assertEq(log, + "get:global," + + "get:unicode," + + "set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,get:result[0],get:lastIndex,set:lastIndex," + + "get:exec,call:exec,"); + +// Trace global with no match. +reset(); +execResult = [ null ]; +lastIndexResult = [ , ]; +lastIndexExpected = [ 0, ]; +ret = RegExp.prototype[Symbol.match].call(myRegExp, target); +assertEq(ret, null); +assertEq(log, + "get:global," + + "get:unicode," + + "set:lastIndex," + + "get:exec,call:exec,"); + +// Trace non-global. +reset(); +global = false; +execResult = [ P(["abc"]) ]; +lastIndexResult = []; +lastIndexExpected = []; +ret = RegExp.prototype[Symbol.match].call(myRegExp, target); +// ret is the Proxy on non-global case, disable logging. +logProxy = false; +assertEq(JSON.stringify(ret), `["abc"]`); +assertEq(log, + "get:global," + + "get:exec,call:exec,"); + +if (typeof reportCompare === "function") + reportCompare(true, true);
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/RegExp/match.js @@ -0,0 +1,36 @@ +var BUGNUMBER = 887016; +var summary = "Implement RegExp.prototype[@@match]."; + +print(BUGNUMBER + ": " + summary); + +assertEq(RegExp.prototype[Symbol.match].name, "[Symbol.match]"); +assertEq(RegExp.prototype[Symbol.match].length, 1); +var desc = Object.getOwnPropertyDescriptor(RegExp.prototype, Symbol.match); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, false); +assertEq(desc.writable, true); + +var re = /a/; +var v = re[Symbol.match]("abcAbcABC"); +assertEq(Array.isArray(v), true); +assertEq(v.length, 1); +assertEq(v[0], "a"); + +re = /d/; +v = re[Symbol.match]("abcAbcABC"); +assertEq(v, null); + +re = /a/ig; +v = re[Symbol.match]("abcAbcABC"); +assertEq(Array.isArray(v), true); +assertEq(v.length, 3); +assertEq(v[0], "a"); +assertEq(v[1], "A"); +assertEq(v[2], "A"); + +re = /d/g; +v = re[Symbol.match]("abcAbcABC"); +assertEq(v, null); + +if (typeof reportCompare === "function") + reportCompare(true, true);
new file mode 100644 --- /dev/null +++ b/js/src/tests/ecma_6/String/match.js @@ -0,0 +1,31 @@ +var BUGNUMBER = 887016; +var summary = "Call RegExp.prototype[@@match] from String.prototype.match."; + +print(BUGNUMBER + ": " + summary); + +var called = 0; +var myRegExp = { + [Symbol.match](S) { + assertEq(S, "abcAbcABC"); + called++; + return 42; + } +}; +assertEq("abcAbcABC".match(myRegExp), 42); +assertEq(called, 1); + +var origMatch = RegExp.prototype[Symbol.match]; + +called = 0; +RegExp.prototype[Symbol.match] = function(S) { + assertEq(S, "abcAbcABC"); + called++; + return 43; +}; +assertEq("abcAbcABC".match("abc"), 43); +assertEq(called, 1); + +RegExp.prototype[Symbol.match] = origMatch; + +if (typeof reportCompare === "function") + reportCompare(true, true);
--- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -450,16 +450,24 @@ GlobalObject::initSelfHostingBuiltins(JS RootedValue std_iterator(cx); std_iterator.setSymbol(cx->wellKnownSymbols().get(JS::SymbolCode::iterator)); if (!JS_DefineProperty(cx, global, "std_iterator", std_iterator, JSPROP_PERMANENT | JSPROP_READONLY)) { return false; } + RootedValue std_match(cx); + std_match.setSymbol(cx->wellKnownSymbols().get(JS::SymbolCode::match)); + if (!JS_DefineProperty(cx, global, "std_match", std_match, + JSPROP_PERMANENT | JSPROP_READONLY)) + { + return false; + } + RootedValue std_species(cx); std_species.setSymbol(cx->wellKnownSymbols().get(JS::SymbolCode::species)); if (!JS_DefineProperty(cx, global, "std_species", std_species, JSPROP_PERMANENT | JSPROP_READONLY)) { return false; }
--- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -13,16 +13,17 @@ #include "jsarray.h" #include "jscntxt.h" #include "jscompartment.h" #include "jsdate.h" #include "jsfriendapi.h" #include "jsfun.h" #include "jshashutil.h" +#include "jsstr.h" #include "jsweakmap.h" #include "jswrapper.h" #include "selfhosted.out.h" #include "builtin/Intl.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" @@ -2069,16 +2070,42 @@ intrinsic_captureCurrentStack(JSContext* RootedObject stack(cx); if (!JS::CaptureCurrentStack(cx, &stack, maxFrameCount)) return false; args.rval().setObject(*stack); return true; } +static bool +IsMatchFlagsArgumentEnabled(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + args.rval().setBoolean(cx->runtime()->options().matchFlagArgument()); + return true; +} + +static bool +WarnOnceAboutFlagsArgument(JSContext* cx, unsigned argc, Value* vp) +{ + if (!cx->compartment()->warnedAboutFlagsArgument) { + if (!JS_ReportErrorFlagsAndNumber(cx, JSREPORT_WARNING, GetErrorMessage, nullptr, + cx->runtime()->options().matchFlagArgument() + ? JSMSG_DEPRECATED_FLAGS_ARG + : JSMSG_OBSOLETE_FLAGS_ARG)) + { + return false; + } + cx->compartment()->warnedAboutFlagsArgument = true; + } + return true; +} + // The self-hosting global isn't initialized with the normal set of builtins. // Instead, individual C++-implemented functions that're required by // self-hosted code are defined as global functions. Accessing these // functions via a content compartment's builtins would be unsafe, because // content script might have changed the builtins' prototypes' members. // Installing the whole set of builtins in the self-hosting compartment, OTOH, // would be wasteful: it increases memory usage and initialization time for // self-hosting compartment. @@ -2126,17 +2153,16 @@ static const JSFunctionSpec intrinsic_fu JS_FN("std_Set_has", SetObject::has, 1,0), JS_FN("std_Set_iterator", SetObject::values, 0,0), JS_INLINABLE_FN("std_String_fromCharCode", str_fromCharCode, 1,0, StringFromCharCode), JS_INLINABLE_FN("std_String_charCodeAt", str_charCodeAt, 1,0, StringCharCodeAt), JS_FN("std_String_indexOf", str_indexOf, 1,0), JS_FN("std_String_lastIndexOf", str_lastIndexOf, 1,0), - JS_FN("std_String_match", str_match, 1,0), JS_INLINABLE_FN("std_String_replace", str_replace, 2,0, StringReplace), JS_INLINABLE_FN("std_String_split", str_split, 2,0, StringSplit), JS_FN("std_String_startsWith", str_startsWith, 1,0), JS_FN("std_String_toLowerCase", str_toLowerCase, 0,0), JS_FN("std_String_toUpperCase", str_toUpperCase, 0,0), JS_FN("std_WeakMap_has", WeakMap_has, 1,0), JS_FN("std_WeakMap_get", WeakMap_get, 2,0), @@ -2389,21 +2415,26 @@ static const JSFunctionSpec intrinsic_fu JS_INLINABLE_FN("RegExpTester", RegExpTester, 4,0, RegExpTester), JS_FN("RegExpCreate", intrinsic_RegExpCreate, 2,0), JS_INLINABLE_FN("RegExpPrototypeOptimizable", RegExpPrototypeOptimizable, 1,0, RegExpPrototypeOptimizable), JS_INLINABLE_FN("RegExpInstanceOptimizable", RegExpInstanceOptimizable, 1,0, RegExpInstanceOptimizable), + JS_FN("FlatStringMatch", FlatStringMatch, 2,0), + // See builtin/RegExp.h for descriptions of the regexp_* functions. JS_FN("regexp_exec_no_statics", regexp_exec_no_statics, 2,0), JS_FN("regexp_test_no_statics", regexp_test_no_statics, 2,0), JS_FN("regexp_construct", regexp_construct_self_hosting, 2,0), + JS_FN("IsMatchFlagsArgumentEnabled", IsMatchFlagsArgumentEnabled, 0,0), + JS_FN("WarnOnceAboutFlagsArgument", WarnOnceAboutFlagsArgument, 0,0), + JS_FN("IsModule", intrinsic_IsInstanceOfBuiltin<ModuleObject>, 1, 0), JS_FN("CallModuleMethodIfWrapped", CallNonGenericSelfhostedMethod<Is<ModuleObject>>, 2, 0), JS_FN("HostResolveImportedModule", intrinsic_HostResolveImportedModule, 2, 0), JS_FN("GetModuleEnvironment", intrinsic_GetModuleEnvironment, 1, 0), JS_FN("CreateModuleEnvironment", intrinsic_CreateModuleEnvironment, 1, 0), JS_FN("CreateImportBinding", intrinsic_CreateImportBinding, 4, 0), JS_FN("CreateNamespaceBinding", intrinsic_CreateNamespaceBinding, 3, 0),
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul +++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul @@ -214,16 +214,17 @@ https://bugzilla.mozilla.org/show_bug.cg gPrototypeProperties['Function'] = ["constructor", "toSource", "toString", "apply", "call", "bind", "isGenerator", "length", "name", "arguments", "caller"]; gConstructorProperties['Function'] = constructorProps([]) gPrototypeProperties['RegExp'] = ["constructor", "toSource", "toString", "compile", "exec", "test", + Symbol.match, "flags", "global", "ignoreCase", "multiline", "source", "sticky", "unicode", "lastIndex"]; gConstructorProperties['RegExp'] = constructorProps(["input", "lastMatch", "lastParen", "leftContext", "rightContext", "$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$_", "$&", "$+", "$`", "$'"])