commit aa2685ed07d7404e3293e0df9d8497b9217c1d5f parent 867009dc644627bcfc520004fc5436e181f898b7 Author: Serah Nderi <snderi@igalia.com> Date: Fri, 19 Dec 2025 22:13:02 +0000 Bug 1306461 - Normalise and partially disable RegExp legacy features, r=iain Differential Revision: https://phabricator.services.mozilla.com/D264695 Diffstat:
44 files changed, 482 insertions(+), 81 deletions(-)
diff --git a/js/public/friend/ErrorNumbers.msg b/js/public/friend/ErrorNumbers.msg @@ -689,6 +689,10 @@ MSG_DEF(JSMSG_INVALID_CLASS_SET_OP, 0, JSEXN_SYNTAXERR, "invalid class se MSG_DEF(JSMSG_INVALID_CHAR_IN_CLASS, 0, JSEXN_SYNTAXERR, "invalid character in class in regular expression") MSG_DEF(JSMSG_NEGATED_CLASS_WITH_STR, 0, JSEXN_SYNTAXERR, "negated character class with strings in regular expression") MSG_DEF(JSMSG_MULTIPLE_FLAG_DASHES, 0, JSEXN_SYNTAXERR, "multiple dashes in flag group") +MSG_DEF(JSMSG_INCOMPATIBLE_RECEIVER, 1, JSEXN_TYPEERR, "RegExp static property '{0}' called with incompatible receiver") +MSG_DEF(JSMSG_REGEXP_STATIC_EMPTY, 1, JSEXN_TYPEERR, "RegExp static property '{0}' is invalid") +MSG_DEF(JSMSG_REGEXP_CROSS_REALM, 0, JSEXN_TYPEERR, "RegExp operation not permitted on object from different realm") +MSG_DEF(JSMSG_REGEXP_LEGACY_FEATURES_DISABLED, 0, JSEXN_TYPEERR, "RegExp legacy features are disabled") // Typed object MSG_DEF(JSMSG_TYPEDOBJECT_SETTING_IMMUTABLE, 0, JSEXN_ERR, "setting immutable field") diff --git a/js/src/builtin/RegExp.cpp b/js/src/builtin/RegExp.cpp @@ -20,6 +20,7 @@ #include "js/PropertySpec.h" #include "js/RegExpFlags.h" // JS::RegExpFlag, JS::RegExpFlags #include "util/StringBuilder.h" +#include "vm/EqualityOperations.h" #include "vm/Interpreter.h" #include "vm/JSContext.h" #include "vm/RegExpObject.h" @@ -317,6 +318,27 @@ static int32_t CreateRegExpSearchResult(JSContext* cx, cx->regExpSearcherLastLimit = matches[0].limit; return matches[0].start; } +/* + * https://github.com/tc39/proposal-regexp-legacy-features/blob/master/README.md#regexpbuiltinexec--r-s- + * + */ + +static bool ShouldUpdateRegExpStatics(JSContext* cx, + Handle<RegExpObject*> regexp) { + if (!JS::Prefs::experimental_legacy_regexp()) { + return true; + } + // Step 5. Let thisRealm be the current Realm Record. + JS::Realm* thisRealm = cx->realm(); + // Step 6. Let rRealm be the value of R's [[Realm]] internal slot. + JS::Realm* rRealm = regexp->realm(); + + // Step 7. If SameValue(thisRealm, rRealm) is true, then + if (thisRealm == rRealm) { + return regexp->legacyFeaturesEnabled(); + } + return false; +} /* * ES 2017 draft rev 6a13789aa9e7c6de4e96b7d3e24d9e6eba6584ad 21.2.5.2.2 @@ -326,14 +348,19 @@ static RegExpRunStatus ExecuteRegExpImpl(JSContext* cx, RegExpStatics* res, MutableHandleRegExpShared re, Handle<JSLinearString*> input, size_t searchIndex, - VectorMatchPairs* matches) { + VectorMatchPairs* matches, + Handle<RegExpObject*> regexp) { RegExpRunStatus status = RegExpShared::execute(cx, re, input, searchIndex, matches); /* Out of spec: Update RegExpStatics. */ if (status == RegExpRunStatus::Success && res) { - if (!res->updateFromMatchPairs(cx, input, *matches)) { - return RegExpRunStatus::Error; + if (ShouldUpdateRegExpStatics(cx, regexp)) { + if (!res->updateFromMatchPairs(cx, input, *matches)) { + return RegExpRunStatus::Error; + } + } else { + res->invalidate(); } } return status; @@ -354,7 +381,7 @@ bool js::ExecuteRegExpLegacy(JSContext* cx, RegExpStatics* res, VectorMatchPairs matches; RegExpRunStatus status = - ExecuteRegExpImpl(cx, res, &shared, input, *lastIndex, &matches); + ExecuteRegExpImpl(cx, res, &shared, input, *lastIndex, &matches, reobj); if (status == RegExpRunStatus::Error) { return false; } @@ -474,9 +501,10 @@ static bool RegExpInitializeIgnoringLastIndex(JSContext* cx, /* ES 2016 draft Mar 25, 2016 21.2.3.2.3. */ bool js::RegExpCreate(JSContext* cx, HandleValue patternValue, - HandleValue flagsValue, MutableHandleValue rval) { + HandleValue flagsValue, MutableHandleValue rval, + HandleObject newTarget) { /* Step 1. */ - Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject)); + Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject, newTarget)); if (!regexp) { return false; } @@ -546,21 +574,26 @@ static bool SetLastIndex(JSContext* cx, Handle<RegExpObject*> regexp, return SetProperty(cx, regexp, cx->names().lastIndex, val); } -/* ES6 B.2.5.1. */ +/* + * RegExp.prototype.compile ( pattern, flags ) + * https://github.com/tc39/proposal-regexp-legacy-features?tab=readme-ov-file#regexpprototypecompile--pattern-flags- + * ES6 B.2.5.1. + */ MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, const CallArgs& args) { MOZ_ASSERT(IsRegExpObject(args.thisv())); Rooted<RegExpObject*> regexp(cx, &args.thisv().toObject().as<RegExpObject>()); - // Step 3. + // Step 7. If Type(pattern) is Object and pattern has a [[RegExpMatcher]] + // internal slot, then RootedValue patternValue(cx, args.get(0)); ESClass cls; if (!GetClassOfValue(cx, patternValue, &cls)) { return false; } if (cls == ESClass::RegExp) { - // Step 3a. + // Step 7.i. If flags is not undefined, throw a TypeError exception. if (args.hasDefined(1)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NEWREGEXP_FLAGGED); @@ -575,7 +608,8 @@ MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, Rooted<JSAtom*> sourceAtom(cx); RegExpFlags flags = RegExpFlag::NoFlags; { - // Step 3b. + // Step 7.ii. Let P be the value of pattern’s [[OriginalSource]] internal + // slot. RegExpShared* shared = RegExpToShared(cx, patternObj); if (!shared) { return false; @@ -585,20 +619,20 @@ MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, flags = shared->getFlags(); } - // Step 5, minus lastIndex zeroing. + // Step 9, minus lastIndex zeroing. regexp->initIgnoringLastIndex(sourceAtom, flags); } else { - // Step 4. + // Step 8. RootedValue P(cx, patternValue); RootedValue F(cx, args.get(1)); - // Step 5, minus lastIndex zeroing. + // Step 9, minus lastIndex zeroing. if (!RegExpInitializeIgnoringLastIndex(cx, regexp, P, F)) { return false; } } - // The final niggling bit of step 5. + // The final niggling bit of step 8. // // |regexp| is user-exposed, so its "lastIndex" property might be // non-writable. @@ -613,6 +647,38 @@ MOZ_ALWAYS_INLINE bool regexp_compile_impl(JSContext* cx, static bool regexp_compile(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); + if (JS::Prefs::experimental_legacy_regexp() && args.thisv().isObject()) { + RootedObject thisObj(cx, &args.thisv().toObject()); + + JSObject* unwrapped = js::CheckedUnwrapStatic(thisObj); + + if (unwrapped && unwrapped->is<RegExpObject>()) { + // Step 3. Let thisRealm be the current Realm Record. + JS::Realm* thisRealm = cx->realm(); + + // Step 4. Let oRealm be the value of O’s [[Realm]] internal slot. + RegExpObject* regexp = &unwrapped->as<RegExpObject>(); + + JS::Realm* oRealm = regexp->realm(); + + // Step 5. If SameValue(thisRealm, oRealm) is false, throw a TypeError + // exception. + if (thisRealm != oRealm) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_REGEXP_CROSS_REALM); + return false; + } + + // Step 6. If the value of R’s [[LegacyFeaturesEnabled]] internal slot is + // false, throw a TypeError exception. + if (!regexp->legacyFeaturesEnabled()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_REGEXP_LEGACY_FEATURES_DISABLED); + return false; + } + } + } + /* Steps 1-2. */ return CallNonGenericMethod<IsRegExpObject, regexp_compile_impl>(cx, args); } @@ -624,15 +690,14 @@ bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "RegExp"); CallArgs args = CallArgsFromVp(argc, vp); + RootedObject newTarget(cx); + // Steps 1. bool patternIsRegExp; if (!IsRegExp(cx, args.get(0), &patternIsRegExp)) { return false; } - // We can delay step 3 and step 4a until later, during - // GetPrototypeFromBuiltinConstructor calls. Accessing the new.target - // and the callee from the stack is unobservable. if (!args.isConstructing()) { // Step 3.b. if (patternIsRegExp && !args.hasDefined(1)) { @@ -652,6 +717,8 @@ bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { return true; } } + } else { + newTarget = &args.newTarget().toObject(); } RootedValue patternValue(cx, args.get(0)); @@ -693,7 +760,8 @@ bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { return false; } - Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject, proto)); + Rooted<RegExpObject*> regexp( + cx, RegExpAlloc(cx, GenericObject, proto, newTarget)); if (!regexp) { return false; } @@ -769,7 +837,8 @@ bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { return false; } - Rooted<RegExpObject*> regexp(cx, RegExpAlloc(cx, GenericObject, proto)); + Rooted<RegExpObject*> regexp( + cx, RegExpAlloc(cx, GenericObject, proto, newTarget)); if (!regexp) { return false; } @@ -790,7 +859,7 @@ bool js::regexp_construct(JSContext* cx, unsigned argc, Value* vp) { */ bool js::regexp_construct_raw_flags(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args.length() == 3); MOZ_ASSERT(!args.isConstructing()); // Step 4.a. @@ -800,7 +869,10 @@ bool js::regexp_construct_raw_flags(JSContext* cx, unsigned argc, Value* vp) { } // Step 4.c. - RegExpFlags flags = AssertedCast<uint8_t>(int32_t(args[1].toNumber())); + uint32_t rawFlags = args[1].toInt32(); + JS::RegExpFlags flags = + AssertedCast<uint8_t>(rawFlags & RegExpFlag::AllFlags); + bool legacy = args[2].toBoolean(); // Step 7. RegExpObject* regexp = RegExpAlloc(cx, GenericObject); @@ -810,6 +882,7 @@ bool js::regexp_construct_raw_flags(JSContext* cx, unsigned argc, Value* vp) { // Step 8. regexp->initAndZeroLastIndex(sourceAtom, flags, cx); + regexp->setLegacyFeaturesEnabled(legacy); args.rval().setObject(*regexp); return true; } @@ -1304,11 +1377,45 @@ static bool regexp_escape(JSContext* cx, unsigned argc, Value* vp) { * RegExp.rightContext $' */ +static bool checkRegexpLegacyFeatures(JSContext* cx, const CallArgs& args, + const char* name) { + if (JS::Prefs::experimental_legacy_regexp()) { + /* Step 1. Assert C is an object that has an internal slot named + * internalSlotName.*/ + JSObject* regexpCtor = + GlobalObject::getOrCreateRegExpConstructor(cx, cx->global()); + if (!regexpCtor) return false; + + /* Step 2. If SameValue(C, thisValue) is false, throw TypeError */ + bool same = false; + if (!args.thisv().isObject() || + !SameValue(cx, args.thisv(), ObjectValue(*regexpCtor), &same) || + !same) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_RECEIVER, name, + InformalValueTypeName(args.thisv())); + return false; + } + + /* Step 4. If val is empty, throw a TypeError exception */ + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); + if (!res) return false; + if (res->isInvalidated()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_REGEXP_STATIC_EMPTY, name, + InformalValueTypeName(args.thisv())); + return false; + } + } + return true; +} + #define DEFINE_STATIC_GETTER(name, code) \ static bool name(JSContext* cx, unsigned argc, Value* vp) { \ CallArgs args = CallArgsFromVp(argc, vp); \ RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); \ if (!res) return false; \ + if (!checkRegexpLegacyFeatures(cx, args, #name)) return false; \ code; \ } @@ -1335,29 +1442,80 @@ DEFINE_STATIC_GETTER(static_paren9_getter, STATIC_PAREN_GETTER_CODE(9)) #define DEFINE_STATIC_SETTER(name, code) \ static bool name(JSContext* cx, unsigned argc, Value* vp) { \ + CallArgs args = CallArgsFromVp(argc, vp); \ RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); \ if (!res) return false; \ + if (!checkRegexpLegacyFeatures(cx, args, #name)) return false; \ code; \ return true; \ } static bool static_input_setter(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); + if (JS::Prefs::experimental_legacy_regexp()) { + // Step 1. Assert C is an object that has an internal slot named + // internalSlotName. + JSObject* regexpCtor = + GlobalObject::getOrCreateRegExpConstructor(cx, cx->global()); + if (!regexpCtor) { + return false; + } + + // Step 2. If SameValue(C, thisValue) is false, throw a TypeError exception. + bool same = false; + if (!args.thisv().isObject() || + !SameValue(cx, args.thisv(), ObjectValue(*regexpCtor), &same) || + !same) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INCOMPATIBLE_RECEIVER, + InformalValueTypeName(args.thisv())); + return false; + } + } + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); if (!res) { return false; } + // Step 3. Let strVal be ? ToString(val). RootedString str(cx, ToString<CanGC>(cx, args.get(0))); if (!str) { return false; } + // Step 4. Set the value of the internal slot of C named internalSlotName to + // strVal. res->setPendingInput(str); args.rval().setString(str); return true; } +#ifdef NIGHTLY_BUILD +const JSPropertySpec js::regexp_static_props[] = { + JS_PSGS("input", static_input_getter, static_input_setter, 0), + JS_PSG("lastMatch", static_lastMatch_getter, 0), + JS_PSG("lastParen", static_lastParen_getter, 0), + JS_PSG("leftContext", static_leftContext_getter, 0), + JS_PSG("rightContext", static_rightContext_getter, 0), + JS_PSG("$1", static_paren1_getter, 0), + JS_PSG("$2", static_paren2_getter, 0), + JS_PSG("$3", static_paren3_getter, 0), + JS_PSG("$4", static_paren4_getter, 0), + JS_PSG("$5", static_paren5_getter, 0), + JS_PSG("$6", static_paren6_getter, 0), + JS_PSG("$7", static_paren7_getter, 0), + JS_PSG("$8", static_paren8_getter, 0), + JS_PSG("$9", static_paren9_getter, 0), + JS_PSGS("$_", static_input_getter, static_input_setter, 0), + JS_PSG("$&", static_lastMatch_getter, 0), + JS_PSG("$+", static_lastParen_getter, 0), + JS_PSG("$`", static_leftContext_getter, 0), + JS_PSG("$'", static_rightContext_getter, 0), + JS_SELF_HOSTED_SYM_GET(species, "$RegExpSpecies", 0), + JS_PS_END, +}; +#else const JSPropertySpec js::regexp_static_props[] = { JS_PSGS("input", static_input_getter, static_input_setter, JSPROP_PERMANENT | JSPROP_ENUMERATE), @@ -1386,6 +1544,7 @@ const JSPropertySpec js::regexp_static_props[] = { JS_SELF_HOSTED_SYM_GET(species, "$RegExpSpecies", 0), JS_PS_END, }; +#endif const JSFunctionSpec js::regexp_static_methods[] = { JS_FN("escape", regexp_escape, 1, 0), @@ -1430,13 +1589,12 @@ static RegExpRunStatus ExecuteRegExp(JSContext* cx, HandleObject regexp, /* Steps 3, 10-14, except 12.a.i, 12.c.i.1. */ RegExpRunStatus status = - ExecuteRegExpImpl(cx, res, &re, input, lastIndex, matches); + ExecuteRegExpImpl(cx, res, &re, input, lastIndex, matches, reobj); if (status == RegExpRunStatus::Error) { return RegExpRunStatus::Error; } /* Steps 12.a.i, 12.c.i.i, 15 are done by Self-hosted function. */ - return status; } @@ -2145,6 +2303,10 @@ bool js::RegExpBuiltinExec(JSContext* cx, Handle<RegExpObject*> regexp, static_assert(JSString::MAX_LENGTH <= INT32_MAX, "lastIndex fits in int32_t"); // Steps 6, 8-35. + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); + if (!res) { + return false; + } if (forTest) { bool result; @@ -2152,6 +2314,7 @@ bool js::RegExpBuiltinExec(JSContext* cx, Handle<RegExpObject*> regexp, &result)) { return false; } + rval.setBoolean(result); return true; } diff --git a/js/src/builtin/RegExp.h b/js/src/builtin/RegExp.h @@ -114,7 +114,8 @@ JSObject* InitRegExpClass(JSContext* cx, HandleObject obj); [[nodiscard]] extern bool RegExpCreate(JSContext* cx, HandleValue pattern, HandleValue flags, - MutableHandleValue rval); + MutableHandleValue rval, + HandleObject newTarget); [[nodiscard]] extern bool IsRegExpPrototypeOptimizable(JSContext* cx, unsigned argc, diff --git a/js/src/builtin/RegExp.js b/js/src/builtin/RegExp.js @@ -949,7 +949,7 @@ function RegExpSplit(string, limit) { if (optimizable) { // Step 5. flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); - + assert(!!(flags & REGEXP_LEGACY_FEATURES_ENABLED_FLAG), "Legacy features must be enabled in optimized path"); // Steps 6-7. unicodeMatching = !!(flags & REGEXP_UNICODE_FLAG); @@ -957,7 +957,8 @@ function RegExpSplit(string, limit) { // If split operation is optimizable, perform non-sticky match. if (flags & REGEXP_STICKY_FLAG) { var source = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT); - splitter = RegExpConstructRaw(source, flags & ~REGEXP_STICKY_FLAG); + var newFlags = flags & ~(REGEXP_STICKY_FLAG | REGEXP_LEGACY_FEATURES_ENABLED_FLAG); + splitter = RegExpConstructRaw(source, newFlags, true); } else { splitter = rx; } @@ -1214,6 +1215,7 @@ function RegExpMatchAll(string) { // Step 5, 9-12. source = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT); flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT); + assert(!!(flags & REGEXP_LEGACY_FEATURES_ENABLED_FLAG), "Legacy features must be enabled in optimized path"); // Step 6. matcher = rx; @@ -1238,6 +1240,9 @@ function RegExpMatchAll(string) { (callFunction(std_String_includes, flags, "g") ? REGEXP_GLOBAL_FLAG : 0) | (callFunction(std_String_includes, flags, "u") ? REGEXP_UNICODE_FLAG : 0); + if (C === builtinCtor) { + flags |= REGEXP_LEGACY_FEATURES_ENABLED_FLAG; + } // Take the non-optimized path. lastIndex = REGEXP_STRING_ITERATOR_LASTINDEX_SLOW; } @@ -1395,7 +1400,8 @@ function RegExpStringIteratorNext() { } // Reify the RegExp object. - regexp = RegExpConstructRaw(source, flags); + var newFlags = flags & ~REGEXP_LEGACY_FEATURES_ENABLED_FLAG; + regexp = RegExpConstructRaw(source, newFlags, true); regexp.lastIndex = lastIndex; UnsafeSetReservedSlot(obj, REGEXP_STRING_ITERATOR_REGEXP_SLOT, regexp); diff --git a/js/src/builtin/RegExpGlobalReplaceOpt.h.js b/js/src/builtin/RegExpGlobalReplaceOpt.h.js @@ -191,7 +191,9 @@ function FUNC_NAME( originalSource || UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT) !== originalFlags ) { - rx = RegExpConstructRaw(originalSource, originalFlags); + var legacy = !!(originalFlags & REGEXP_LEGACY_FEATURES_ENABLED_FLAG); + var newFlags = originalFlags & ~REGEXP_LEGACY_FEATURES_ENABLED_FLAG; + rx = RegExpConstructRaw(originalSource, newFlags, legacy); } #endif } diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h @@ -111,6 +111,7 @@ #define REGEXP_DOTALL_FLAG 0x20 #define REGEXP_HASINDICES_FLAG 0x40 #define REGEXP_UNICODESETS_FLAG 0x80 +#define REGEXP_LEGACY_FEATURES_ENABLED_FLAG 0x100 #define REGEXP_STRING_ITERATOR_REGEXP_SLOT 0 #define REGEXP_STRING_ITERATOR_STRING_SLOT 1 diff --git a/js/src/jit-test/tests/basic/testCrossCompartmentTransparency.js b/js/src/jit-test/tests/basic/testCrossCompartmentTransparency.js @@ -76,7 +76,6 @@ if (String.prototype.toSource) { test("new String('one')", s => String.prototype.toString.call(s)); test("new RegExp('1')", r => RegExp.prototype.exec.call(r, '1').toString()); test("new RegExp('1')", r => RegExp.prototype.test.call(r, '1')); -test("new RegExp('1')", r => RegExp.prototype.compile.call(r, '1').toString()); test("new RegExp('1')", r => assertEq("a1".search(r), 1)); test("new RegExp('1')", r => assertEq("a1".match(r)[0], '1')); test("new RegExp('1')", r => assertEq("a1".replace(r, 'A'), 'aA')); diff --git a/js/src/jit-test/tests/regexp/legacy-features-enabled.js b/js/src/jit-test/tests/regexp/legacy-features-enabled.js @@ -0,0 +1,43 @@ +// |jit-test| --enable-legacy-regexp +function testLegacyFeaturesEnabled(re, str) { + for (let i = 0; i < 100; i++){ + re.test(str); + } +} + +{ + class MyRegExp extends RegExp {} + + var re1 = /a(b)c/; + testLegacyFeaturesEnabled(re1, "abc"); + assertEq(RegExp.$1, "b"); + + var re2 = new MyRegExp("d(e)f"); + testLegacyFeaturesEnabled(re2, "def"); + + var threw = false; + try { + RegExp.$1; + } catch(e) { + threw = true; + assertEq(e instanceof TypeError, true); + } +} + +{ + var other = newGlobal(); + other.eval("var re = /x(y)z/;"); + + var re = other.re; + testLegacyFeaturesEnabled(re, "xyz"); + + var threw = false; + try { + RegExp.$1; + } catch(e) { + threw = true; + assertEq(e instanceof TypeError, true); + } + + assertEq(threw, true); +} diff --git a/js/src/jit-test/tests/regexp/regexp-subclass-proto-jit-bug.js b/js/src/jit-test/tests/regexp/regexp-subclass-proto-jit-bug.js @@ -0,0 +1,24 @@ +// |jit-test| --enable-legacy-regexp; skip-if: !getBuildConfiguration("debug") + +load(libdir + 'asserts.js'); + +function testSubclassProtoJitBug() { + class MyRegExp extends RegExp {} + let real = RegExp("ab|cd"); + let fake = new MyRegExp("ab|cd"); + fake.__proto__ = RegExp.prototype; + + function foo(r) { + return r.test("ab"); + } + + for (var i = 0; i < 2000; i++) { + foo(real); + } + assertEq(RegExp.lastMatch, "ab"); + + foo(fake); + assertThrowsInstanceOf(() => RegExp.lastMatch, TypeError); +} + +testSubclassProtoJitBug(); diff --git a/js/src/jit/CodeGenerator.cpp b/js/src/jit/CodeGenerator.cpp @@ -2227,6 +2227,21 @@ static void UpdateRegExpStatics(MacroAssembler& masm, Register regexp, RegExpStatics::offsetOfMatchesInput()); Address lazySourceAddress(staticsReg, RegExpStatics::offsetOfLazySource()); Address lazyIndexAddress(staticsReg, RegExpStatics::offsetOfLazyIndex()); + Label legacyFeaturesEnabled, done; + if (JS::Prefs::experimental_legacy_regexp()) { + Address invalidatedAddress(staticsReg, + RegExpStatics::offsetOfInvalidated()); + + masm.unboxNonDouble(Address(regexp, NativeObject::getFixedSlotOffset( + RegExpObject::flagsSlot())), + temp1, JSVAL_TYPE_INT32); + masm.branchTest32(Assembler::NonZero, temp1, + Imm32(RegExpObject::LegacyFeaturesEnabledBit), + &legacyFeaturesEnabled); + masm.store8(Imm32(1), invalidatedAddress); + masm.jump(&done); + masm.bind(&legacyFeaturesEnabled); + } masm.guardedCallPreBarrier(pendingInputAddress, MIRType::String); masm.guardedCallPreBarrier(matchesInputAddress, MIRType::String); @@ -2271,6 +2286,7 @@ static void UpdateRegExpStatics(MacroAssembler& masm, Register regexp, static_assert(sizeof(JS::RegExpFlags) == 1, "load size must match flag size"); masm.load8ZeroExtend(Address(temp1, RegExpShared::offsetOfFlags()), temp2); masm.store8(temp2, Address(staticsReg, RegExpStatics::offsetOfLazyFlags())); + masm.bind(&done); } // Prepare an InputOutputData and optional MatchPairs which space has been diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp @@ -13375,7 +13375,9 @@ bool InitOptionParser(OptionParser& op) { "Enable immutable ArrayBuffers") || !op.addBoolOption('\0', "enable-iterator-chunking", "Enable Iterator Chunking") || - !op.addBoolOption('\0', "enable-iterator-join", "Enable Iterator.join")) { + !op.addBoolOption('\0', "enable-iterator-join", "Enable Iterator.join") || + !op.addBoolOption('\0', "enable-legacy-regexp", + "Enable Legacy RegExp features")) { return false; } @@ -13435,6 +13437,9 @@ bool SetGlobalOptionsPreJSInit(const OptionParser& op) { JS::Prefs::setAtStartup_experimental_joint_iteration(true); } + if (op.getBoolOption("enable-legacy-regexp")) { + JS::Prefs::set_experimental_legacy_regexp(true); + } #ifdef NIGHTLY_BUILD if (op.getBoolOption("enable-async-iterator-helpers")) { JS::Prefs::setAtStartup_experimental_async_iterator_helpers(true); diff --git a/js/src/tests/non262/regress/regress-591846.js b/js/src/tests/non262/regress/regress-591846.js @@ -1,11 +1,13 @@ +// |reftest| shell-option(--enable-legacy-regexp) skip-if(release_or_beta||!xulRuntime.shell) -- legacy-regexp is not released yet, requires shell-options /* * Any copyright is dedicated to the Public Domain. * http://creativecommons.org/licenses/publicdomain/ */ // SKIP test262 export -// Note that the values for enumerable/configurable are inconsistent with -// https://github.com/tc39/proposal-regexp-legacy-features +// This test verifies that RegExp legacy static properties match TC39's expected +// attributes: non-enumerable and configurable. +// See: https://github.com/tc39/proposal-regexp-legacy-features // Reset RegExp.leftContext to the empty string. /x/.test('x'); @@ -13,8 +15,9 @@ var d = Object.getOwnPropertyDescriptor(RegExp, "leftContext"); assertEq(d.set, undefined); assertEq(typeof d.get, "function"); -assertEq(d.enumerable, true); -assertEq(d.configurable, false); +let regexpLegacyFeatures = getPrefValue('experimental.legacy_regexp'); +assertEq(d.enumerable, !regexpLegacyFeatures); +assertEq(d.configurable, regexpLegacyFeatures); assertEq(d.get.call(RegExp), ""); reportCompare(0, 0, "ok"); diff --git a/js/src/tests/test262-update.py b/js/src/tests/test262-update.py @@ -18,7 +18,6 @@ UNSUPPORTED_FEATURES = set( [ "tail-call-optimization", "Intl.Locale-info", # Bug 1693576 - "legacy-regexp", # Bug 1306461 "source-phase-imports", "source-phase-imports-module-source", "import-defer", @@ -39,7 +38,7 @@ FEATURE_CHECK_NEEDED = { "upsert": "!Map.prototype.getOrInsertComputed", # Bug 1986668 "immutable-arraybuffer": "!ArrayBuffer.prototype.sliceToImmutable", # Bug 1952253 } -RELEASE_OR_BETA = set() +RELEASE_OR_BETA = set(["legacy-regexp"]) SHELL_OPTIONS = { "ShadowRealm": "--enable-shadow-realms", "symbols-as-weakmap-keys": "--enable-symbols-as-weakmap-keys", diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/index/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/input/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastMatch/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/lastParen/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/leftContext/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/prop-desc.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/prop-desc.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-cross-realm-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-cross-realm-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-not-regexp-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-not-regexp-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-subclass-constructor.js b/js/src/tests/test262/annexB/built-ins/RegExp/legacy-accessors/rightContext/this-subclass-constructor.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/prototype/compile/this-cross-realm-instance.js b/js/src/tests/test262/annexB/built-ins/RegExp/prototype/compile/this-cross-realm-instance.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/tests/test262/annexB/built-ins/RegExp/prototype/compile/this-subclass-instance.js b/js/src/tests/test262/annexB/built-ins/RegExp/prototype/compile/this-subclass-instance.js @@ -1,4 +1,4 @@ -// |reftest| skip -- legacy-regexp is not supported +// |reftest| skip-if(release_or_beta) -- legacy-regexp is not released yet // Copyright (C) 2020 ExE Boss. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- diff --git a/js/src/vm/RegExpObject.cpp b/js/src/vm/RegExpObject.cpp @@ -62,9 +62,12 @@ static_assert(RegExpFlag::UnicodeSets == REGEXP_UNICODESETS_FLAG, "self-hosted JS and /v flag bits must agree"); static_assert(RegExpFlag::Sticky == REGEXP_STICKY_FLAG, "self-hosted JS and /y flag bits must agree"); - +/* + * RegExpAlloc ( newTarget ) + * https://github.com/tc39/proposal-regexp-legacy-features?tab=readme-ov-file + */ RegExpObject* js::RegExpAlloc(JSContext* cx, NewObjectKind newKind, - HandleObject proto /* = nullptr */) { + HandleObject proto, HandleObject newTarget) { Rooted<RegExpObject*> regexp( cx, NewObjectWithClassProtoAndKind<RegExpObject>(cx, proto, newKind)); if (!regexp) { @@ -74,10 +77,34 @@ RegExpObject* js::RegExpAlloc(JSContext* cx, NewObjectKind newKind, if (!SharedShape::ensureInitialCustomShape<RegExpObject>(cx, regexp)) { return nullptr; } - + // Step 1. Let obj be ? OrdinaryCreateFromConstructor(newTarget, + // "%RegExpPrototype%", «[[RegExpMatcher]], [[OriginalSource]], + // [[OriginalFlags]], [[Realm]], [[LegacyFeaturesEnabled]]»). + // Set default newTarget if not provided + bool legacyFeaturesEnabled = false; + if (JS::Prefs::experimental_legacy_regexp()) { + // Step 2. Let thisRealm be the current Realm Record. + // Step 3. Set the value of obj’s [[Realm]] internal slot to thisRealm. + JS::Realm* thisRealm = cx->realm(); + + JSObject* thisRealmRegExp = + &thisRealm->maybeGlobal()->getConstructor(JSProto_RegExp); + + // Step 4. If SameValue(newTarget, thisRealm.[[Intrinsics]].[[%RegExp%]]) is + // true, Step 4.i then Set the value of obj’s [[LegacyFeaturesEnabled]] + // internal slot to true. Step 5. Else, Step 5.i. Set the value of obj’s + // [[LegacyFeaturesEnabled]] internal slot to false. + legacyFeaturesEnabled = (!newTarget || newTarget == thisRealmRegExp); + } + regexp->setLegacyFeaturesEnabled(legacyFeaturesEnabled); + + // Step 6: Perform ! DefinePropertyOrThrow(obj, "lastIndex", + // PropertyDescriptor {[[Writable]]: true, [Enumerable]]: false, + // [[Configurable]]: false}). MOZ_ASSERT(regexp->lookupPure(cx->names().lastIndex)->slot() == RegExpObject::lastIndexSlot()); + // Step 7: Return obj. return regexp; } @@ -145,7 +172,8 @@ const JSClass RegExpObject::protoClass_ = { template <typename CharT> RegExpObject* RegExpObject::create(JSContext* cx, const CharT* chars, size_t length, RegExpFlags flags, - NewObjectKind newKind) { + NewObjectKind newKind, + HandleObject newTarget) { static_assert(std::is_same_v<CharT, char16_t>, "this code may need updating if/when CharT encodes UTF-8"); @@ -154,19 +182,21 @@ RegExpObject* RegExpObject::create(JSContext* cx, const CharT* chars, return nullptr; } - return create(cx, source, flags, newKind); + return create(cx, source, flags, newKind, newTarget); } template RegExpObject* RegExpObject::create(JSContext* cx, const char16_t* chars, size_t length, RegExpFlags flags, - NewObjectKind newKind); + NewObjectKind newKind, + HandleObject newTarget); RegExpObject* RegExpObject::createSyntaxChecked(JSContext* cx, Handle<JSAtom*> source, RegExpFlags flags, - NewObjectKind newKind) { - RegExpObject* regexp = RegExpAlloc(cx, newKind); + NewObjectKind newKind, + HandleObject newTarget) { + RegExpObject* regexp = RegExpAlloc(cx, newKind, nullptr, newTarget); if (!regexp) { return nullptr; } @@ -177,7 +207,8 @@ RegExpObject* RegExpObject::createSyntaxChecked(JSContext* cx, } RegExpObject* RegExpObject::create(JSContext* cx, Handle<JSAtom*> source, - RegExpFlags flags, NewObjectKind newKind) { + RegExpFlags flags, NewObjectKind newKind, + HandleObject newTarget) { Rooted<RegExpObject*> regexp(cx); { AutoReportFrontendContext fc(cx); @@ -190,7 +221,7 @@ RegExpObject* RegExpObject::create(JSContext* cx, Handle<JSAtom*> source, return nullptr; } - regexp = RegExpAlloc(cx, newKind); + regexp = RegExpAlloc(cx, newKind, nullptr, newTarget); if (!regexp) { return nullptr; } @@ -1088,7 +1119,9 @@ JSObject* js::CloneRegExpObject(JSContext* cx, Handle<RegExpObject*> regex) { clone->initAndZeroLastIndex(shared->getSource(), shared->getFlags(), cx); clone->setShared(shared); - + if (JS::Prefs::experimental_legacy_regexp()) { + clone->setLegacyFeaturesEnabled(regex->legacyFeaturesEnabled()); + } return clone; } diff --git a/js/src/vm/RegExpObject.h b/js/src/vm/RegExpObject.h @@ -36,7 +36,8 @@ class GenericPrinter; class JSONPrinter; extern RegExpObject* RegExpAlloc(JSContext* cx, NewObjectKind newKind, - HandleObject proto = nullptr); + HandleObject proto = nullptr, + HandleObject newTarget = nullptr); extern JSObject* CloneRegExpObject(JSContext* cx, Handle<RegExpObject*> regex); @@ -61,23 +62,32 @@ class RegExpObject : public NativeObject { static const JSClass class_; static const JSClass protoClass_; + static const size_t RegExpFlagsMask = JS::RegExpFlag::AllFlags; + static const size_t LegacyFeaturesEnabledBit = Bit(8); + + static_assert((RegExpFlagsMask & LegacyFeaturesEnabledBit) == 0, + "LegacyFeaturesEnabledBit must not overlap"); + // The maximum number of pairs a MatchResult can have, without having to // allocate a bigger MatchResult. static const size_t MaxPairCount = 14; template <typename CharT> static RegExpObject* create(JSContext* cx, const CharT* chars, size_t length, - JS::RegExpFlags flags, NewObjectKind newKind); + JS::RegExpFlags flags, NewObjectKind newKind, + HandleObject newTarget = nullptr); // This variant assumes that the characters have already previously been // syntax checked. static RegExpObject* createSyntaxChecked(JSContext* cx, Handle<JSAtom*> source, JS::RegExpFlags flags, - NewObjectKind newKind); + NewObjectKind newKind, + HandleObject newTarget = nullptr); static RegExpObject* create(JSContext* cx, Handle<JSAtom*> source, - JS::RegExpFlags flags, NewObjectKind newKind); + JS::RegExpFlags flags, NewObjectKind newKind, + HandleObject newTarget = nullptr); /* * Compute the initial shape to associate with fresh RegExp objects, @@ -139,10 +149,41 @@ class RegExpObject : public NativeObject { } JS::RegExpFlags getFlags() const { - return JS::RegExpFlags(getFixedSlot(FLAGS_SLOT).toInt32()); + Value flagsVal = getFixedSlot(FLAGS_SLOT); + uint32_t raw = flagsVal.toInt32(); + return JS::RegExpFlags(raw & RegExpFlagsMask); } + void setFlags(JS::RegExpFlags flags) { - setFixedSlot(FLAGS_SLOT, Int32Value(flags.value())); + Value flagsVal = getFixedSlot(FLAGS_SLOT); + uint32_t raw = 0; + if (flagsVal.isInt32()) { + raw = static_cast<uint32_t>(flagsVal.toInt32()); + } + uint32_t newValue = flags.value() | (raw & ~RegExpFlagsMask); + setFixedSlot(FLAGS_SLOT, Int32Value(newValue)); + } + + bool legacyFeaturesEnabled() const { + if (!JS::Prefs::experimental_legacy_regexp()) { + return false; + } + return (getFixedSlot(FLAGS_SLOT).toInt32() & LegacyFeaturesEnabledBit); + } + + void setLegacyFeaturesEnabled(bool enabled) { + MOZ_ASSERT_IF(enabled, JS::Prefs::experimental_legacy_regexp()); + Value flagsVal = getFixedSlot(FLAGS_SLOT); + uint32_t raw = 0; + if (flagsVal.isInt32()) { + raw = static_cast<uint32_t>(flagsVal.toInt32()); + } + if (enabled) { + raw |= LegacyFeaturesEnabledBit; + } else { + raw &= ~LegacyFeaturesEnabledBit; + } + setFixedSlot(FLAGS_SLOT, Int32Value(raw)); } bool hasIndices() const { return getFlags().hasIndices(); } diff --git a/js/src/vm/RegExpStatics.h b/js/src/vm/RegExpStatics.h @@ -46,6 +46,30 @@ class RegExpStatics { inline void checkInvariants(); + // Legacy RegExp static properties support. + private: + bool invalidated_ = false; + + public: + bool isInvalidated() const { + if (!JS::Prefs::experimental_legacy_regexp()) { + return false; + } + return invalidated_; + } + + inline void invalidate() { + if (JS::Prefs::experimental_legacy_regexp()) { + invalidated_ = true; + } + } + + inline void clearInvalidation() { + if (JS::Prefs::experimental_legacy_regexp()) { + invalidated_ = false; + } + } + /* * Check whether a match for pair |pairNum| occurred. If so, allocate and * store the match string in |*out|; otherwise place |undefined| in |*out|. @@ -117,6 +141,10 @@ class RegExpStatics { static size_t offsetOfPendingLazyEvaluation() { return offsetof(RegExpStatics, pendingLazyEvaluation); } + + static size_t offsetOfInvalidated() { + return offsetof(RegExpStatics, invalidated_); + } }; inline bool RegExpStatics::createDependent(JSContext* cx, size_t start, @@ -162,6 +190,12 @@ inline bool RegExpStatics::createLastMatch(JSContext* cx, if (!executeLazy(cx)) { return false; } + + if (isInvalidated()) { + out.setUndefined(); + return true; + } + return makeMatch(cx, 0, out); } @@ -171,6 +205,11 @@ inline bool RegExpStatics::createLastParen(JSContext* cx, return false; } + if (isInvalidated()) { + out.setUndefined(); + return true; + } + if (matches.empty() || matches.pairCount() == 1) { out.setString(cx->runtime()->emptyString); return true; @@ -192,6 +231,11 @@ inline bool RegExpStatics::createParen(JSContext* cx, size_t pairNum, return false; } + if (isInvalidated()) { + out.setUndefined(); + return true; + } + if (matches.empty() || pairNum >= matches.pairCount()) { out.setString(cx->runtime()->emptyString); return true; @@ -205,6 +249,11 @@ inline bool RegExpStatics::createLeftContext(JSContext* cx, return false; } + if (isInvalidated()) { + out.setUndefined(); + return true; + } + if (matches.empty()) { out.setString(cx->runtime()->emptyString); return true; @@ -222,6 +271,11 @@ inline bool RegExpStatics::createRightContext(JSContext* cx, return false; } + if (isInvalidated()) { + out.setUndefined(); + return true; + } + if (matches.empty()) { out.setString(cx->runtime()->emptyString); return true; @@ -250,7 +304,7 @@ inline bool RegExpStatics::updateFromMatchPairs(JSContext* cx, ReportOutOfMemory(cx); return false; } - + clearInvalidation(); return true; } diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp @@ -1048,7 +1048,7 @@ static bool intrinsic_RegExpCreate(JSContext* cx, unsigned argc, Value* vp) { args[1].isString() || args[1].isUndefined()); MOZ_ASSERT(!args.isConstructing()); - return RegExpCreate(cx, args[0], args.get(1), args.rval()); + return RegExpCreate(cx, args[0], args.get(1), args.rval(), nullptr); } static bool intrinsic_RegExpGetSubstitution(JSContext* cx, unsigned argc, diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -9046,6 +9046,13 @@ mirror: always set_spidermonkey_pref: startup +# Experimental support for Legacy RegExp in JavaScript. +- name: javascript.options.experimental.legacy_regexp + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + set_spidermonkey_pref: always + #ifdef NIGHTLY_BUILD # Experimental support for Async Iterator Helpers in JavaScript. - name: javascript.options.experimental.async_iterator_helpers