Bug 808245, Part 6/6 - Add MatchOnly mode and lazify RegExpStatics. r=dvander
authorSean Stangl <sstangl@mozilla.com>
Wed, 12 Dec 2012 18:11:28 -0800
changeset 116473 b7e2ba73b2ff7851a4a7f8502aa02c3fbb3405bc
parent 116472 7711a36c27717f84038edbfc8a2f3e2788dc46d3
child 116474 bed3d6fd43b0668d8208a18772031b907d8ee063
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersdvander
bugs808245
milestone20.0a1
Bug 808245, Part 6/6 - Add MatchOnly mode and lazify RegExpStatics. r=dvander
js/src/builtin/RegExp.cpp
js/src/gc/Marking.cpp
js/src/gc/Marking.h
js/src/gc/RootMarking.cpp
js/src/vm/RegExpObject.cpp
js/src/vm/RegExpObject.h
js/src/vm/RegExpStatics-inl.h
js/src/vm/RegExpStatics.cpp
js/src/vm/RegExpStatics.h
--- a/js/src/builtin/RegExp.cpp
+++ b/js/src/builtin/RegExp.cpp
@@ -114,24 +114,30 @@ js::CreateRegExpMatchResult(JSContext *c
 }
 
 RegExpRunStatus
 ExecuteRegExpImpl(JSContext *cx, RegExpStatics *res, RegExpShared &re, RegExpObject &regexp,
                   JSLinearString *input, StableCharPtr chars, size_t length,
                   size_t *lastIndex, MatchConduit &matches)
 {
     RegExpRunStatus status;
-    
-    /* Ahem, not handled in this patch. But it was a pain to rip out. */
-    JS_ASSERT(!matches.isPair);
 
-    /* Vector of MatchPairs provided: execute full regexp. */
-    status = re.execute(cx, chars, length, lastIndex, *matches.u.pairs);
-    if (status == RegExpRunStatus_Success && res)
-        res->updateFromMatchPairs(cx, input, *matches.u.pairs);
+    /* Switch between MatchOnly and IncludeSubpatterns modes. */
+    if (matches.isPair) {
+        size_t lastIndex_orig = *lastIndex;
+        /* Only one MatchPair slot provided: execute short-circuiting regexp. */
+        status = re.executeMatchOnly(cx, chars, length, lastIndex, *matches.u.pair);
+        if (status == RegExpRunStatus_Success && res)
+            res->updateLazily(cx, input, &regexp, lastIndex_orig);
+    } else {
+        /* Vector of MatchPairs provided: execute full regexp. */
+        status = re.execute(cx, chars, length, lastIndex, *matches.u.pairs);
+        if (status == RegExpRunStatus_Success && res)
+            res->updateFromMatchPairs(cx, input, *matches.u.pairs);
+    }
 
     return status;
 }
 
 /* Legacy ExecuteRegExp behavior is baked into the JSAPI. */
 bool
 js::ExecuteRegExpLegacy(JSContext *cx, RegExpStatics *res, RegExpObject &reobj,
                         Handle<JSStableString*> input, StableCharPtr chars, size_t length,
--- a/js/src/gc/Marking.cpp
+++ b/js/src/gc/Marking.cpp
@@ -307,16 +307,17 @@ Is##base##AboutToBeFinalized(Encapsulate
 DeclMarkerImpl(BaseShape, BaseShape)
 DeclMarkerImpl(BaseShape, UnownedBaseShape)
 DeclMarkerImpl(IonCode, ion::IonCode)
 DeclMarkerImpl(Object, ArgumentsObject)
 DeclMarkerImpl(Object, DebugScopeObject)
 DeclMarkerImpl(Object, GlobalObject)
 DeclMarkerImpl(Object, JSObject)
 DeclMarkerImpl(Object, JSFunction)
+DeclMarkerImpl(Object, RegExpObject)
 DeclMarkerImpl(Object, ScopeObject)
 DeclMarkerImpl(Script, JSScript)
 DeclMarkerImpl(Shape, Shape)
 DeclMarkerImpl(String, JSAtom)
 DeclMarkerImpl(String, JSString)
 DeclMarkerImpl(String, JSFlatString)
 DeclMarkerImpl(String, JSLinearString)
 DeclMarkerImpl(String, PropertyName)
--- a/js/src/gc/Marking.h
+++ b/js/src/gc/Marking.h
@@ -92,16 +92,17 @@ bool Is##base##AboutToBeFinalized(Encaps
 DeclMarker(BaseShape, BaseShape)
 DeclMarker(BaseShape, UnownedBaseShape)
 DeclMarker(IonCode, ion::IonCode)
 DeclMarker(Object, ArgumentsObject)
 DeclMarker(Object, DebugScopeObject)
 DeclMarker(Object, GlobalObject)
 DeclMarker(Object, JSObject)
 DeclMarker(Object, JSFunction)
+DeclMarker(Object, RegExpObject)
 DeclMarker(Object, ScopeObject)
 DeclMarker(Script, JSScript)
 DeclMarker(Shape, Shape)
 DeclMarker(String, JSAtom)
 DeclMarker(String, JSString)
 DeclMarker(String, JSFlatString)
 DeclMarker(String, JSLinearString)
 DeclMarker(String, PropertyName)
--- a/js/src/gc/RootMarking.cpp
+++ b/js/src/gc/RootMarking.cpp
@@ -669,16 +669,19 @@ Shape::Range::AutoRooter::trace(JSTracer
 {
     if (r->cursor)
         MarkShapeRoot(trc, const_cast<Shape**>(&r->cursor), "Shape::Range::AutoRooter");
 }
 
 void
 RegExpStatics::AutoRooter::trace(JSTracer *trc)
 {
+    if (statics->regexp)
+        MarkObjectRoot(trc, reinterpret_cast<JSObject**>(&statics->regexp),
+                       "RegExpStatics::AutoRooter regexp");
     if (statics->matchesInput)
         MarkStringRoot(trc, reinterpret_cast<JSString**>(&statics->matchesInput),
                        "RegExpStatics::AutoRooter matchesInput");
     if (statics->pendingInput)
         MarkStringRoot(trc, reinterpret_cast<JSString**>(&statics->pendingInput),
                        "RegExpStatics::AutoRooter pendingInput");
 }
 
--- a/js/src/vm/RegExpObject.cpp
+++ b/js/src/vm/RegExpObject.cpp
@@ -443,20 +443,20 @@ RegExpShared::checkSyntax(JSContext *cx,
     if (error == JSC::Yarr::NoError)
         return true;
 
     reportYarrError(cx, tokenStream, error);
     return false;
 }
 
 bool
-RegExpShared::compile(JSContext *cx)
+RegExpShared::compile(JSContext *cx, bool matchOnly)
 {
     if (!sticky())
-        return compile(cx, *source);
+        return compile(cx, *source, matchOnly);
 
     /*
      * The sticky case we implement hackily by prepending a caret onto the front
      * and relying on |::execute| to pseudo-slice the string when it sees a sticky regexp.
      */
     static const jschar prefix[] = {'^', '(', '?', ':'};
     static const jschar postfix[] = {')'};
 
@@ -467,21 +467,21 @@ RegExpShared::compile(JSContext *cx)
     sb.infallibleAppend(prefix, ArrayLength(prefix));
     sb.infallibleAppend(source->chars(), source->length());
     sb.infallibleAppend(postfix, ArrayLength(postfix));
 
     JSAtom *fakeySource = sb.finishAtom();
     if (!fakeySource)
         return false;
 
-    return compile(cx, *fakeySource);
+    return compile(cx, *fakeySource, matchOnly);
 }
 
 bool
-RegExpShared::compile(JSContext *cx, JSLinearString &pattern)
+RegExpShared::compile(JSContext *cx, JSLinearString &pattern, bool matchOnly)
 {
     /* Parse the pattern. */
     ErrorCode yarrError;
     YarrPattern yarrPattern(pattern, ignoreCase(), multiline(), &yarrError);
     if (yarrError) {
         reportYarrError(cx, NULL, yarrError);
         return false;
     }
@@ -489,17 +489,18 @@ RegExpShared::compile(JSContext *cx, JSL
 
 #if ENABLE_YARR_JIT
     if (isJITRuntimeEnabled(cx) && !yarrPattern.m_containsBackreferences) {
         JSC::ExecutableAllocator *execAlloc = cx->runtime->getExecAlloc(cx);
         if (!execAlloc)
             return false;
 
         JSGlobalData globalData(execAlloc);
-        YarrJITCompileMode compileMode = JSC::Yarr::IncludeSubpatterns;
+        YarrJITCompileMode compileMode = matchOnly ? JSC::Yarr::MatchOnly
+                                                   : JSC::Yarr::IncludeSubpatterns;
 
         jitCompile(yarrPattern, JSC::Yarr::Char16, &globalData, codeBlock, compileMode);
 
         /* Unset iff the Yarr JIT compilation was successful. */
         if (!codeBlock.isFallBack())
             return true;
     }
     codeBlock.setFallBack(true);
@@ -515,17 +516,25 @@ RegExpShared::compile(JSContext *cx, JSL
     return true;
 }
 
 bool
 RegExpShared::compileIfNecessary(JSContext *cx)
 {
     if (hasCode() || hasBytecode())
         return true;
-    return compile(cx);
+    return compile(cx, false);
+}
+
+bool
+RegExpShared::compileMatchOnlyIfNecessary(JSContext *cx)
+{
+    if (hasMatchOnlyCode() || hasBytecode())
+        return true;
+    return compile(cx, true);
 }
 
 RegExpRunStatus
 RegExpShared::execute(JSContext *cx, StableCharPtr chars, size_t length,
                       size_t *lastIndex, MatchPairs &matches)
 {
     /* Compile the code at point-of-use. */
     if (!compileIfNecessary(cx))
@@ -566,16 +575,72 @@ RegExpShared::execute(JSContext *cx, Sta
         return RegExpRunStatus_Success_NotFound;
 
     matches.displace(displacement);
     matches.checkAgainst(origLength);
     *lastIndex = matches[0].limit;
     return RegExpRunStatus_Success;
 }
 
+RegExpRunStatus
+RegExpShared::executeMatchOnly(JSContext *cx, StableCharPtr chars, size_t length,
+                               size_t *lastIndex, MatchPair &match)
+{
+    /* Compile the code at point-of-use. */
+    if (!compileMatchOnlyIfNecessary(cx))
+        return RegExpRunStatus_Error;
+
+    const size_t origLength = length;
+    size_t start = *lastIndex;
+    size_t displacement = 0;
+
+    if (sticky()) {
+        displacement = start;
+        chars += displacement;
+        length -= displacement;
+        start = 0;
+    }
+
+#if ENABLE_YARR_JIT
+    if (!codeBlock.isFallBack()) {
+        MatchResult result = codeBlock.execute(chars.get(), start, length);
+        if (!result)
+            return RegExpRunStatus_Success_NotFound;
+
+        match = MatchPair(result.start, result.end);
+        match.displace(displacement);
+        *lastIndex = match.limit;
+        return RegExpRunStatus_Success;
+    }
+#endif
+
+    /*
+     * The JIT could not be used, so fall back to the Yarr interpreter.
+     * Unfortunately, the interpreter does not have a MatchOnly mode, so a
+     * temporary output vector must be provided.
+     */
+    JS_ASSERT(hasBytecode());
+    ScopedMatchPairs matches(&cx->tempLifoAlloc());
+    if (!matches.initArray(pairCount()))
+        return RegExpRunStatus_Error;
+
+    unsigned result =
+        JSC::Yarr::interpret(bytecode, chars.get(), length, start, matches.rawBuf());
+
+    if (result == JSC::Yarr::offsetNoMatch)
+        return RegExpRunStatus_Success_NotFound;
+
+    matches.displace(displacement);
+    matches.checkAgainst(origLength);
+
+    *lastIndex = matches[0].limit;
+    match = MatchPair(result, matches[0].limit);
+    return RegExpRunStatus_Success;
+}
+
 /* RegExpCompartment */
 
 RegExpCompartment::RegExpCompartment(JSRuntime *rt)
   : map_(rt), inUse_(rt)
 {}
 
 RegExpCompartment::~RegExpCompartment()
 {
--- a/js/src/vm/RegExpObject.h
+++ b/js/src/vm/RegExpObject.h
@@ -134,20 +134,21 @@ class RegExpShared
 #endif
     BytecodePattern *bytecode;
 
     /* Lifetime-preserving variables: see class-level comment above. */
     size_t             activeUseCount;
     uint64_t           gcNumberWhenUsed;
 
     /* Internal functions. */
-    bool compile(JSContext *cx);
-    bool compile(JSContext *cx, JSLinearString &pattern);
+    bool compile(JSContext *cx, bool matchOnly);
+    bool compile(JSContext *cx, JSLinearString &pattern, bool matchOnly);
 
     bool compileIfNecessary(JSContext *cx);
+    bool compileMatchOnlyIfNecessary(JSContext *cx);
 
   public:
     RegExpShared(JSRuntime *rt, JSAtom *source, RegExpFlag flags);
     ~RegExpShared();
 
     /* Static functions to expose some Yarr logic. */
     static inline bool isJITRuntimeEnabled(JSContext *cx);
     static void reportYarrError(JSContext *cx, TokenStream *ts, ErrorCode error);
@@ -155,16 +156,20 @@ class RegExpShared
 
     /* Called when a RegExpShared is installed into a RegExpObject. */
     inline void prepareForUse(JSContext *cx);
 
     /* Primary interface: run this regular expression on the given string. */
     RegExpRunStatus execute(JSContext *cx, StableCharPtr chars, size_t length,
                             size_t *lastIndex, MatchPairs &matches);
 
+    /* Run the regular expression without collecting matches, for test(). */
+    RegExpRunStatus executeMatchOnly(JSContext *cx, StableCharPtr chars, size_t length,
+                                     size_t *lastIndex, MatchPair &match);
+
     /* Accessors */
 
     size_t getParenCount() const        { JS_ASSERT(isCompiled()); return parenCount; }
     void incRef()                       { activeUseCount++; }
     void decRef()                       { JS_ASSERT(activeUseCount > 0); activeUseCount--; }
 
     /* Accounts for the "0" (whole match) pair. */
     size_t pairCount() const            { return getParenCount() + 1; }
@@ -172,21 +177,23 @@ class RegExpShared
     RegExpFlag getFlags() const         { return flags; }
     bool ignoreCase() const             { return flags & IgnoreCaseFlag; }
     bool global() const                 { return flags & GlobalFlag; }
     bool multiline() const              { return flags & MultilineFlag; }
     bool sticky() const                 { return flags & StickyFlag; }
 
 #ifdef ENABLE_YARR_JIT
     bool hasCode() const                { return codeBlock.has16BitCode(); }
+    bool hasMatchOnlyCode() const       { return codeBlock.has16BitCodeMatchOnly(); }
 #else
     bool hasCode() const                { return false; }
+    bool hasMatchOnlyCode() const       { return false; }
 #endif
     bool hasBytecode() const            { return bytecode != NULL; }
-    bool isCompiled() const             { return hasBytecode() || hasCode(); }
+    bool isCompiled() const             { return hasBytecode() || hasCode() || hasMatchOnlyCode(); }
 };
 
 /*
  * Extend the lifetime of a given RegExpShared to at least the lifetime of
  * the guard object. See Regular Expression comment at the top.
  */
 class RegExpGuard
 {
@@ -247,17 +254,16 @@ class RegExpCompartment
 
   public:
     RegExpCompartment(JSRuntime *rt);
     ~RegExpCompartment();
 
     bool init(JSContext *cx);
     void sweep(JSRuntime *rt);
 
-    /* Return a regexp corresponding to the given (source, flags) pair. */
     bool get(JSContext *cx, JSAtom *source, RegExpFlag flags, RegExpGuard *g);
 
     /* Like 'get', but compile 'maybeOpt' (if non-null). */
     bool get(JSContext *cx, JSAtom *source, JSString *maybeOpt, RegExpGuard *g);
 
     size_t sizeOfExcludingThis(JSMallocSizeOfFun mallocSizeOf);
 };
 
--- a/js/src/vm/RegExpStatics-inl.h
+++ b/js/src/vm/RegExpStatics-inl.h
@@ -24,25 +24,28 @@ js::GlobalObject::getRegExpStatics() con
 inline size_t
 SizeOfRegExpStaticsData(const JSObject *obj, JSMallocSizeOfFun mallocSizeOf)
 {
     return mallocSizeOf(obj->getPrivate());
 }
 
 inline
 RegExpStatics::RegExpStatics()
-  : bufferLink(NULL),
+  : pendingLazyEvaluation(false),
+    bufferLink(NULL),
     copied(false)
 {
     clear();
 }
 
 inline bool
 RegExpStatics::createDependent(JSContext *cx, size_t start, size_t end, Value *out)
 {
+    /* Private function: caller must perform lazy evaluation. */
+    JS_ASSERT(!pendingLazyEvaluation);
 
     JS_ASSERT(start <= end);
     JS_ASSERT(end <= matchesInput->length());
     JSString *str = js_NewDependentString(cx, matchesInput, start, end - start);
     if (!str)
         return false;
     *out = StringValue(str);
     return true;
@@ -54,38 +57,46 @@ RegExpStatics::createPendingInput(JSCont
     /* Lazy evaluation need not be resolved to return the input. */
     out->setString(pendingInput ? pendingInput.get() : cx->runtime->emptyString);
     return true;
 }
 
 inline bool
 RegExpStatics::makeMatch(JSContext *cx, size_t checkValidIndex, size_t pairNum, Value *out)
 {
+    /* Private function: caller must perform lazy evaluation. */
+    JS_ASSERT(!pendingLazyEvaluation);
+
     bool checkWhich  = checkValidIndex % 2;
     size_t checkPair = checkValidIndex / 2;
 
     if (matches.empty() || checkPair >= matches.pairCount() ||
         (checkWhich ? matches[checkPair].limit : matches[checkPair].start) < 0)
     {
         out->setString(cx->runtime->emptyString);
         return true;
     }
     const MatchPair &pair = matches[pairNum];
     return createDependent(cx, pair.start, pair.limit, out);
 }
 
 inline bool
 RegExpStatics::createLastMatch(JSContext *cx, Value *out)
 {
+    if (!executeLazy(cx))
+        return false;
     return makeMatch(cx, 0, 0, out);
 }
 
 inline bool
 RegExpStatics::createLastParen(JSContext *cx, Value *out)
 {
+    if (!executeLazy(cx))
+        return false;
+
     if (matches.empty() || matches.pairCount() == 1) {
         out->setString(cx->runtime->emptyString);
         return true;
     }
     const MatchPair &pair = matches[matches.pairCount() - 1];
     if (pair.start == -1) {
         out->setString(cx->runtime->emptyString);
         return true;
@@ -94,118 +105,146 @@ RegExpStatics::createLastParen(JSContext
     JS_ASSERT(pair.limit >= pair.start);
     return createDependent(cx, pair.start, pair.limit, out);
 }
 
 inline bool
 RegExpStatics::createParen(JSContext *cx, size_t pairNum, Value *out)
 {
     JS_ASSERT(pairNum >= 1);
+    if (!executeLazy(cx))
+        return false;
+
     if (matches.empty() || pairNum >= matches.pairCount()) {
         out->setString(cx->runtime->emptyString);
         return true;
     }
     return makeMatch(cx, pairNum * 2, pairNum, out);
 }
 
 inline bool
 RegExpStatics::createLeftContext(JSContext *cx, Value *out)
 {
+    if (!executeLazy(cx))
+        return false;
+
     if (matches.empty()) {
         out->setString(cx->runtime->emptyString);
         return true;
     }
     if (matches[0].start < 0) {
         *out = UndefinedValue();
         return true;
     }
     return createDependent(cx, 0, matches[0].start, out);
 }
 
 inline bool
 RegExpStatics::createRightContext(JSContext *cx, Value *out)
 {
+    if (!executeLazy(cx))
+        return false;
+
     if (matches.empty()) {
         out->setString(cx->runtime->emptyString);
         return true;
     }
     if (matches[0].limit < 0) {
         *out = UndefinedValue();
         return true;
     }
     return createDependent(cx, matches[0].limit, matchesInput->length(), out);
 }
 
 inline void
 RegExpStatics::getParen(size_t pairNum, JSSubString *out) const
 {
+    JS_ASSERT(!pendingLazyEvaluation);
+
     JS_ASSERT(pairNum >= 1 && pairNum < matches.pairCount());
     const MatchPair &pair = matches[pairNum];
     if (pair.isUndefined()) {
         *out = js_EmptySubString;
         return;
     }
     out->chars  = matchesInput->chars() + pair.start;
     out->length = pair.length();
 }
 
 inline void
 RegExpStatics::getLastMatch(JSSubString *out) const
 {
+    JS_ASSERT(!pendingLazyEvaluation);
+
     if (matches.empty()) {
         *out = js_EmptySubString;
         return;
     }
     JS_ASSERT(matchesInput);
     out->chars = matchesInput->chars() + matches[0].start;
     JS_ASSERT(matches[0].limit >= matches[0].start);
     out->length = matches[0].length();
 }
 
 inline void
 RegExpStatics::getLastParen(JSSubString *out) const
 {
+    JS_ASSERT(!pendingLazyEvaluation);
+
     /* Note: the first pair is the whole match. */
     if (matches.empty() || matches.pairCount() == 1) {
         *out = js_EmptySubString;
         return;
     }
     getParen(matches.parenCount(), out);
 }
 
 inline void
 RegExpStatics::getLeftContext(JSSubString *out) const
 {
+    JS_ASSERT(!pendingLazyEvaluation);
+
     if (matches.empty()) {
         *out = js_EmptySubString;
         return;
     }
     out->chars = matchesInput->chars();
     out->length = matches[0].start;
 }
 
 inline void
 RegExpStatics::getRightContext(JSSubString *out) const
 {
+    JS_ASSERT(!pendingLazyEvaluation);
+
     if (matches.empty()) {
         *out = js_EmptySubString;
         return;
     }
     out->chars = matchesInput->chars() + matches[0].limit;
     JS_ASSERT(matches[0].limit <= int(matchesInput->length()));
     out->length = matchesInput->length() - matches[0].limit;
 }
 
 inline void
 RegExpStatics::copyTo(RegExpStatics &dst)
 {
-    dst.matches.initArrayFrom(matches);
+    /* Destination buffer has already been reserved by save(). */
+    if (!pendingLazyEvaluation)
+        dst.matches.initArrayFrom(matches);
+
     dst.matchesInput = matchesInput;
+    dst.regexp = regexp;
+    dst.lastIndex = lastIndex;
     dst.pendingInput = pendingInput;
     dst.flags = flags;
+    dst.pendingLazyEvaluation = pendingLazyEvaluation;
+
+    JS_ASSERT_IF(pendingLazyEvaluation, regexp);
+    JS_ASSERT_IF(pendingLazyEvaluation, matchesInput);
 }
 
 inline void
 RegExpStatics::aboutToWrite()
 {
     if (bufferLink && !bufferLink->copied) {
         copyTo(*bufferLink);
         bufferLink->copied = true;
@@ -215,22 +254,42 @@ RegExpStatics::aboutToWrite()
 inline void
 RegExpStatics::restore()
 {
     if (bufferLink->copied)
         bufferLink->copyTo(*this);
     bufferLink = bufferLink->bufferLink;
 }
 
+inline void
+RegExpStatics::updateLazily(JSContext *cx, JSLinearString *input,
+                            RegExpObject *regexp, size_t lastIndex)
+{
+    JS_ASSERT(input && regexp);
+    aboutToWrite();
+
+    BarrieredSetPair<JSString, JSLinearString>(cx->compartment,
+                                               pendingInput, input,
+                                               matchesInput, input);
+    pendingLazyEvaluation = true;
+    this->regexp = regexp;
+    this->lastIndex = lastIndex;
+}
+
 inline bool
 RegExpStatics::updateFromMatchPairs(JSContext *cx, JSLinearString *input, MatchPairs &newPairs)
 {
     JS_ASSERT(input);
     aboutToWrite();
 
+    /* Unset all lazy state. */
+    pendingLazyEvaluation = false;
+    this->regexp = NULL;
+    this->lastIndex = size_t(-1);
+
     BarrieredSetPair<JSString, JSLinearString>(cx->compartment,
                                                pendingInput, input,
                                                matchesInput, input);
 
     if (!matches.initArrayFrom(newPairs)) {
         js_ReportOutOfMemory(cx);
         return false;
     }
@@ -239,16 +298,17 @@ RegExpStatics::updateFromMatchPairs(JSCo
 }
 
 inline void
 RegExpStatics::clear()
 {
     aboutToWrite();
     flags = RegExpFlag(0);
     pendingInput = NULL;
+    pendingLazyEvaluation = false;
     matchesInput = NULL;
     matches.forgetArray();
 }
 
 inline void
 RegExpStatics::setPendingInput(JSString *newInput)
 {
     aboutToWrite();
@@ -297,16 +357,22 @@ RegExpStatics::reset(JSContext *cx, JSSt
     setMultiline(cx, newMultiline);
     checkInvariants();
 }
 
 inline void
 RegExpStatics::checkInvariants()
 {
 #ifdef DEBUG
+    if (pendingLazyEvaluation) {
+        JS_ASSERT(regexp);
+        JS_ASSERT(pendingInput);
+        return;
+    }
+
     if (matches.empty()) {
         JS_ASSERT(!matchesInput);
         return;
     }
 
     /* Pair count is non-zero, so there must be match pairs input. */
     JS_ASSERT(matchesInput);
     size_t mpiLen = matchesInput->length();
--- a/js/src/vm/RegExpStatics.cpp
+++ b/js/src/vm/RegExpStatics.cpp
@@ -62,8 +62,42 @@ RegExpStatics::create(JSContext *cx, Glo
     if (!obj)
         return NULL;
     RegExpStatics *res = cx->new_<RegExpStatics>();
     if (!res)
         return NULL;
     obj->setPrivate(static_cast<void *>(res));
     return obj;
 }
+
+bool
+RegExpStatics::executeLazy(JSContext *cx)
+{
+    if (!pendingLazyEvaluation)
+        return true;
+
+    JS_ASSERT(regexp);
+    JS_ASSERT(matchesInput);
+    JS_ASSERT(lastIndex != size_t(-1));
+
+    /*
+     * It is not necessary to call aboutToWrite(): evaluation of
+     * implicit copies is safe.
+     */
+
+    size_t length = matchesInput->length();
+    StableCharPtr chars(matchesInput->chars(), length);
+
+    /* Execute the full regular expression. */
+    RegExpGuard shared;
+    if (!regexp->getShared(cx, &shared))
+        return false;
+
+    RegExpRunStatus status = shared->execute(cx, chars, length, &this->lastIndex, this->matches);
+    if (status == RegExpRunStatus_Error)
+        return false;
+
+    /* Unset lazy state and remove rooted values that now have no use. */
+    pendingLazyEvaluation = false;
+    regexp = NULL;
+
+    return true;
+}
--- a/js/src/vm/RegExpStatics.h
+++ b/js/src/vm/RegExpStatics.h
@@ -19,25 +19,37 @@
 namespace js {
 
 class RegExpStatics
 {
     /* The latest RegExp output, set after execution. */
     VectorMatchPairs        matches;
     HeapPtr<JSLinearString> matchesInput;
 
+    /* The previous RegExp input, used to resolve lazy state. */
+    HeapPtr<RegExpObject>   regexp;
+    size_t                  lastIndex;
+
     /* The latest RegExp input, set before execution. */
     HeapPtr<JSString>       pendingInput;
     RegExpFlag              flags;
 
+    /*
+     * If true, |matchesInput|, |regexp|, and |lastIndex| may be used
+     * to replay the last executed RegExp, and |matches| is invalid.
+     */
+    bool                    pendingLazyEvaluation;
+
     /* Linkage for preserving RegExpStatics during nested RegExp execution. */
     RegExpStatics           *bufferLink;
     bool                    copied;
 
   private:
+    bool executeLazy(JSContext *cx);
+
     inline void aboutToWrite();
     inline void copyTo(RegExpStatics &dst);
 
     inline void restore();
     bool save(JSContext *cx, RegExpStatics *buffer) {
         JS_ASSERT(!buffer->copied && !buffer->bufferLink);
         buffer->bufferLink = bufferLink;
         bufferLink = buffer;
@@ -67,44 +79,52 @@ class RegExpStatics
 
   public:
     inline RegExpStatics();
 
     static JSObject *create(JSContext *cx, GlobalObject *parent);
 
     /* Mutators. */
 
+    inline void updateLazily(JSContext *cx, JSLinearString *input,
+                             RegExpObject *regexp, size_t lastIndex);
     inline bool updateFromMatchPairs(JSContext *cx, JSLinearString *input, MatchPairs &newPairs);
     inline void setMultiline(JSContext *cx, bool enabled);
 
     inline void clear();
 
     /* Corresponds to JSAPI functionality to set the pending RegExp input. */
     inline void reset(JSContext *cx, JSString *newInput, bool newMultiline);
 
     inline void setPendingInput(JSString *newInput);
 
   public:
     /* Default match accessor. */
     const MatchPairs &getMatches() const {
+        /* Safe: only used by String methods, which do not set lazy mode. */
+        JS_ASSERT(!pendingLazyEvaluation);
         return matches;
     }
 
     JSString *getPendingInput() const { return pendingInput; }
 
     RegExpFlag getFlags() const { return flags; }
     bool multiline() const { return flags & MultilineFlag; }
 
     /* Returns whether results for a non-empty match are present. */
     bool matched() const {
+        /* Safe: only used by String methods, which do not set lazy mode. */
+        JS_ASSERT(!pendingLazyEvaluation);
         JS_ASSERT(matches.pairCount() > 0);
         return matches[0].limit - matches[0].start > 0;
     }
 
     void mark(JSTracer *trc) {
+        if (regexp)
+            gc::MarkObject(trc, &regexp, "res->regexp");
         if (pendingInput)
             MarkString(trc, &pendingInput, "res->pendingInput");
         if (matchesInput)
             MarkString(trc, &matchesInput, "res->matchesInput");
     }
 
     /* Value creators. */