[regexp] Ensure there are no shape changes on the fast path

BUG=v8:5437,chromium:708247

Review-Url: https://codereview.chromium.org/2797993002
Cr-Commit-Position: refs/heads/master@{#44428}
diff --git a/src/builtins/builtins-regexp-gen.cc b/src/builtins/builtins-regexp-gen.cc
index b76ee8c..d87fdf6 100644
--- a/src/builtins/builtins-regexp-gen.cc
+++ b/src/builtins/builtins-regexp-gen.cc
@@ -482,7 +482,9 @@
   Node* const int_zero = IntPtrConstant(0);
   Node* const smi_zero = SmiConstant(Smi::kZero);
 
-  if (!is_fastpath) {
+  if (is_fastpath) {
+    CSA_ASSERT(this, IsFastRegExpNoPrototype(context, regexp, LoadMap(regexp)));
+  } else {
     ThrowIfNotInstanceType(context, regexp, JS_REGEXP_TYPE,
                            "RegExp.prototype.exec");
   }
@@ -499,18 +501,23 @@
     Node* const regexp_lastindex = LoadLastIndex(context, regexp, is_fastpath);
     var_lastindex.Bind(regexp_lastindex);
 
-    // Omit ToLength if lastindex is a non-negative smi.
-    Label call_tolength(this, Label::kDeferred), next(this);
-    Branch(TaggedIsPositiveSmi(regexp_lastindex), &next, &call_tolength);
+    if (is_fastpath) {
+      // ToLength on a positive smi is a nop and can be skipped.
+      CSA_ASSERT(this, TaggedIsPositiveSmi(regexp_lastindex));
+    } else {
+      // Omit ToLength if lastindex is a non-negative smi.
+      Label call_tolength(this, Label::kDeferred), next(this);
+      Branch(TaggedIsPositiveSmi(regexp_lastindex), &next, &call_tolength);
 
-    Bind(&call_tolength);
-    {
-      var_lastindex.Bind(
-          CallBuiltin(Builtins::kToLength, context, regexp_lastindex));
-      Goto(&next);
+      Bind(&call_tolength);
+      {
+        var_lastindex.Bind(
+            CallBuiltin(Builtins::kToLength, context, regexp_lastindex));
+        Goto(&next);
+      }
+
+      Bind(&next);
     }
-
-    Bind(&next);
   }
 
   // Check whether the regexp is global or sticky, which determines whether we
@@ -658,7 +665,12 @@
   return var_value_map.value();
 }
 
-Node* RegExpBuiltinsAssembler::IsInitialRegExpMap(Node* context, Node* map) {
+Node* RegExpBuiltinsAssembler::IsFastRegExpNoPrototype(Node* const context,
+                                                       Node* const object,
+                                                       Node* const map) {
+  Label out(this);
+  Variable var_result(this, MachineRepresentation::kWord32);
+
   Node* const native_context = LoadNativeContext(context);
   Node* const regexp_fun =
       LoadContextElement(native_context, Context::REGEXP_FUNCTION_INDEX);
@@ -666,17 +678,33 @@
       LoadObjectField(regexp_fun, JSFunction::kPrototypeOrInitialMapOffset);
   Node* const has_initialmap = WordEqual(map, initial_map);
 
-  return has_initialmap;
+  var_result.Bind(has_initialmap);
+  GotoIfNot(has_initialmap, &out);
+
+  // The smi check is required to omit ToLength(lastIndex) calls with possible
+  // user-code execution on the fast path.
+  Node* const last_index = FastLoadLastIndex(object);
+  var_result.Bind(TaggedIsPositiveSmi(last_index));
+  Goto(&out);
+
+  Bind(&out);
+  return var_result.value();
 }
 
 // RegExp fast path implementations rely on unmodified JSRegExp instances.
 // We use a fairly coarse granularity for this and simply check whether both
-// the regexp itself is unmodified (i.e. its map has not changed) and its
-// prototype is unmodified.
+// the regexp itself is unmodified (i.e. its map has not changed), its
+// prototype is unmodified, and lastIndex is a non-negative smi.
 void RegExpBuiltinsAssembler::BranchIfFastRegExp(Node* const context,
+                                                 Node* const object,
                                                  Node* const map,
                                                  Label* const if_isunmodified,
                                                  Label* const if_ismodified) {
+  CSA_ASSERT(this, WordEqual(LoadMap(object), map));
+
+  // TODO(ishell): Update this check once map changes for constant field
+  // tracking are landing.
+
   Node* const native_context = LoadNativeContext(context);
   Node* const regexp_fun =
       LoadContextElement(native_context, Context::REGEXP_FUNCTION_INDEX);
@@ -692,18 +720,21 @@
   Node* const proto_has_initialmap =
       WordEqual(proto_map, initial_proto_initial_map);
 
-  // TODO(ishell): Update this check once map changes for constant field
-  // tracking are landing.
+  GotoIfNot(proto_has_initialmap, if_ismodified);
 
-  Branch(proto_has_initialmap, if_isunmodified, if_ismodified);
+  // The smi check is required to omit ToLength(lastIndex) calls with possible
+  // user-code execution on the fast path.
+  Node* const last_index = FastLoadLastIndex(object);
+  Branch(TaggedIsPositiveSmi(last_index), if_isunmodified, if_ismodified);
 }
 
-Node* RegExpBuiltinsAssembler::IsFastRegExpMap(Node* const context,
-                                               Node* const map) {
+Node* RegExpBuiltinsAssembler::IsFastRegExp(Node* const context,
+                                            Node* const object,
+                                            Node* const map) {
   Label yup(this), nope(this), out(this);
   Variable var_result(this, MachineRepresentation::kWord32);
 
-  BranchIfFastRegExp(context, map, &yup, &nope);
+  BranchIfFastRegExp(context, object, map, &yup, &nope);
 
   Bind(&yup);
   var_result.Bind(Int32Constant(1));
@@ -753,7 +784,7 @@
   Node* const string = ToString(context, maybe_string);
 
   Label if_isfastpath(this), if_isslowpath(this);
-  Branch(IsInitialRegExpMap(context, regexp_map), &if_isfastpath,
+  Branch(IsFastRegExpNoPrototype(context, receiver, regexp_map), &if_isfastpath,
          &if_isslowpath);
 
   Bind(&if_isfastpath);
@@ -960,7 +991,8 @@
   Node* const receiver = maybe_receiver;
 
   Label if_isfastpath(this), if_isslowpath(this, Label::kDeferred);
-  Branch(IsInitialRegExpMap(context, map), &if_isfastpath, &if_isslowpath);
+  Branch(IsFastRegExpNoPrototype(context, receiver, map), &if_isfastpath,
+         &if_isslowpath);
 
   Bind(&if_isfastpath);
   Return(FlagsGetter(context, receiver, true));
@@ -1405,8 +1437,6 @@
 // ES#sec-regexpexec Runtime Semantics: RegExpExec ( R, S )
 Node* RegExpBuiltinsAssembler::RegExpExec(Node* context, Node* regexp,
                                           Node* string) {
-  CSA_ASSERT(this, Word32BinaryNot(IsFastRegExpMap(context, LoadMap(regexp))));
-
   Variable var_result(this, MachineRepresentation::kTagged);
   Label out(this);
 
@@ -1471,7 +1501,7 @@
   Node* const string = ToString(context, maybe_string);
 
   Label fast_path(this), slow_path(this);
-  BranchIfFastRegExp(context, map, &fast_path, &slow_path);
+  BranchIfFastRegExp(context, receiver, map, &fast_path, &slow_path);
 
   Bind(&fast_path);
   {
@@ -1800,12 +1830,24 @@
         Node* const match_length = LoadStringLength(match);
         GotoIfNot(SmiEqual(match_length, smi_zero), &loop);
 
-        Node* const last_index =
-            CallBuiltin(Builtins::kToLength, context,
-                        LoadLastIndex(context, regexp, is_fastpath));
+        Node* last_index = LoadLastIndex(context, regexp, is_fastpath);
+        if (is_fastpath) {
+          CSA_ASSERT(this, TaggedIsPositiveSmi(last_index));
+        } else {
+          last_index = CallBuiltin(Builtins::kToLength, context, last_index);
+        }
+
         Node* const new_last_index =
             AdvanceStringIndex(string, last_index, is_unicode);
 
+        if (is_fastpath) {
+          // On the fast path, we can be certain that lastIndex can never be
+          // incremented to overflow the Smi range since the maximal string
+          // length is less than the maximal Smi value.
+          STATIC_ASSERT(String::kMaxLength < Smi::kMaxValue);
+          CSA_ASSERT(this, TaggedIsPositiveSmi(new_last_index));
+        }
+
         StoreLastIndex(context, regexp, new_last_index, is_fastpath);
 
         Goto(&loop);
@@ -1839,7 +1881,7 @@
   Node* const string = ToString(context, maybe_string);
 
   Label fast_path(this), slow_path(this);
-  BranchIfFastRegExp(context, map, &fast_path, &slow_path);
+  BranchIfFastRegExp(context, receiver, map, &fast_path, &slow_path);
 
   Bind(&fast_path);
   RegExpPrototypeMatchBody(context, receiver, string, true);
@@ -1961,7 +2003,7 @@
   Node* const string = ToString(context, maybe_string);
 
   Label fast_path(this), slow_path(this);
-  BranchIfFastRegExp(context, map, &fast_path, &slow_path);
+  BranchIfFastRegExp(context, receiver, map, &fast_path, &slow_path);
 
   Bind(&fast_path);
   RegExpPrototypeSearchBodyFast(context, receiver, string);
@@ -2219,7 +2261,7 @@
   Node* const maybe_limit = Parameter(Descriptor::kLimit);
   Node* const context = Parameter(Descriptor::kContext);
 
-  CSA_ASSERT(this, IsFastRegExpMap(context, LoadMap(regexp)));
+  CSA_ASSERT(this, IsFastRegExp(context, regexp, LoadMap(regexp)));
   CSA_ASSERT(this, IsString(string));
 
   // TODO(jgruber): Even if map checks send us to the fast path, we still need
@@ -2273,7 +2315,7 @@
   Node* const string = ToString(context, maybe_string);
 
   Label stub(this), runtime(this, Label::kDeferred);
-  BranchIfFastRegExp(context, map, &stub, &runtime);
+  BranchIfFastRegExp(context, receiver, map, &stub, &runtime);
 
   Bind(&stub);
   Return(CallBuiltin(Builtins::kRegExpSplit, context, receiver, string,
@@ -2599,7 +2641,7 @@
   Node* const replace_value = Parameter(Descriptor::kReplaceValue);
   Node* const context = Parameter(Descriptor::kContext);
 
-  CSA_ASSERT(this, IsFastRegExpMap(context, LoadMap(regexp)));
+  CSA_ASSERT(this, IsFastRegExp(context, regexp, LoadMap(regexp)));
   CSA_ASSERT(this, IsString(string));
 
   Label checkreplacestring(this), if_iscallable(this),
@@ -2688,7 +2730,7 @@
 
   // Fast-path checks: 1. Is the {receiver} an unmodified JSRegExp instance?
   Label stub(this), runtime(this, Label::kDeferred);
-  BranchIfFastRegExp(context, map, &stub, &runtime);
+  BranchIfFastRegExp(context, receiver, map, &stub, &runtime);
 
   Bind(&stub);
   Return(CallBuiltin(Builtins::kRegExpReplace, context, receiver, string,
diff --git a/src/builtins/builtins-regexp-gen.h b/src/builtins/builtins-regexp-gen.h
index 440cc6b..7c818e7 100644
--- a/src/builtins/builtins-regexp-gen.h
+++ b/src/builtins/builtins-regexp-gen.h
@@ -15,8 +15,8 @@
   explicit RegExpBuiltinsAssembler(compiler::CodeAssemblerState* state)
       : CodeStubAssembler(state) {}
 
-  void BranchIfFastRegExp(Node* const context, Node* const map,
-                          Label* const if_isunmodified,
+  void BranchIfFastRegExp(Node* const context, Node* const object,
+                          Node* const map, Label* const if_isunmodified,
                           Label* const if_ismodified);
 
  protected:
@@ -58,9 +58,13 @@
                              char const* method_name);
 
   // Analogous to BranchIfFastRegExp, for use in asserts.
-  Node* IsFastRegExpMap(Node* const context, Node* const map);
+  Node* IsFastRegExp(Node* const context, Node* const object, Node* const map);
 
-  Node* IsInitialRegExpMap(Node* context, Node* map);
+  // Performs fast path checks on the given object itself, but omits prototype
+  // checks.
+  Node* IsFastRegExpNoPrototype(Node* const context, Node* const object,
+                                Node* const map);
+
   void BranchIfFastRegExpResult(Node* context, Node* map,
                                 Label* if_isunmodified, Label* if_ismodified);
 
diff --git a/src/builtins/builtins-string-gen.cc b/src/builtins/builtins-string-gen.cc
index 9100739..fcb732b 100644
--- a/src/builtins/builtins-string-gen.cc
+++ b/src/builtins/builtins-string-gen.cc
@@ -969,7 +969,7 @@
     Label stub_call(this), slow_lookup(this);
 
     RegExpBuiltinsAssembler regexp_asm(state());
-    regexp_asm.BranchIfFastRegExp(context, object_map, &stub_call,
+    regexp_asm.BranchIfFastRegExp(context, object, object_map, &stub_call,
                                   &slow_lookup);
 
     Bind(&stub_call);
diff --git a/src/regexp/regexp-utils.cc b/src/regexp/regexp-utils.cc
index d404318..570a348 100644
--- a/src/regexp/regexp-utils.cc
+++ b/src/regexp/regexp-utils.cc
@@ -145,7 +145,14 @@
   if (!proto->IsJSReceiver()) return false;
 
   Handle<Map> initial_proto_initial_map = isolate->regexp_prototype_map();
-  return (JSReceiver::cast(proto)->map() == *initial_proto_initial_map);
+  if (JSReceiver::cast(proto)->map() != *initial_proto_initial_map) {
+    return false;
+  }
+
+  // The smi check is required to omit ToLength(lastIndex) calls with possible
+  // user-code execution on the fast path.
+  Object* last_index = JSRegExp::cast(recv)->LastIndex();
+  return last_index->IsSmi() && Smi::cast(last_index)->value() >= 0;
 }
 
 int RegExpUtils::AdvanceStringIndex(Isolate* isolate, Handle<String> string,
diff --git a/test/mjsunit/regress/regress-708247.js b/test/mjsunit/regress/regress-708247.js
new file mode 100644
index 0000000..7512791
--- /dev/null
+++ b/test/mjsunit/regress/regress-708247.js
@@ -0,0 +1,26 @@
+// Copyright 2017 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --predictable
+
+const str = '2016-01-02';
+
+function t() {
+  var re;
+  function toDictMode() {
+    for (var i = 0; i < 100; i++) {  // Loop is required.
+      re.x = 42;
+      delete re.x;
+    }
+    return 0;
+  }
+
+  re = /-/g;  // Needs to be global to trigger lastIndex accesses.
+  re.lastIndex = { valueOf : toDictMode };
+  return re.exec(str);
+}
+
+for (var q = 0; q < 10000; q++) {
+  t();  // Needs repetitions to trigger a crash.
+}