LiveEdit: calculate a real script difference
authorpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Mon, 19 Apr 2010 16:08:26 +0000 (16:08 +0000)
committerpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Mon, 19 Apr 2010 16:08:26 +0000 (16:08 +0000)
Review URL: http://codereview.chromium.org/1652008

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@4441 ce2b1a6d-e550-0410-aec6-3dcde31c8c00

src/handles.cc
src/handles.h
src/liveedit-debugger.js
src/liveedit.cc
src/liveedit.h
src/runtime.cc
src/runtime.h
test/cctest/SConscript
test/cctest/test-liveedit.cc [new file with mode: 0644]
test/mjsunit/debug-liveedit-diff.js [new file with mode: 0644]

index 05cb3f2..84ee20b 100644 (file)
@@ -457,6 +457,16 @@ void InitScriptLineEnds(Handle<Script> script) {
   }
 
   Handle<String> src(String::cast(script->source()));
+
+  Handle<FixedArray> array = CalculateLineEnds(src, true);
+
+  script->set_line_ends(*array);
+  ASSERT(script->line_ends()->IsFixedArray());
+}
+
+
+Handle<FixedArray> CalculateLineEnds(Handle<String> src,
+                                     bool with_imaginary_last_new_line) {
   const int src_len = src->length();
   Handle<String> new_line = Factory::NewStringFromAscii(CStrVector("\n"));
 
@@ -468,8 +478,12 @@ void InitScriptLineEnds(Handle<Script> script) {
     if (position != -1) {
       position++;
     }
-    // Even if the last line misses a line end, it is counted.
-    line_count++;
+    if (position != -1) {
+      line_count++;
+    } else if (with_imaginary_last_new_line) {
+      // Even if the last line misses a line end, it is counted.
+      line_count++;
+    }
   }
 
   // Pass 2: Fill in line ends positions
@@ -478,15 +492,17 @@ void InitScriptLineEnds(Handle<Script> script) {
   position = 0;
   while (position != -1 && position < src_len) {
     position = Runtime::StringMatch(src, new_line, position);
-    // If the script does not end with a line ending add the final end
-    // position as just past the last line ending.
-    array->set(array_index++,
-               Smi::FromInt(position != -1 ? position++ : src_len));
+    if (position != -1) {
+      array->set(array_index++, Smi::FromInt(position++));
+    } else if (with_imaginary_last_new_line) {
+      // If the script does not end with a line ending add the final end
+      // position as just past the last line ending.
+      array->set(array_index++, Smi::FromInt(src_len));
+    }
   }
   ASSERT(array_index == line_count);
 
-  script->set_line_ends(*array);
-  ASSERT(script->line_ends()->IsFixedArray());
+  return array;
 }
 
 
index 54c3b45..5baceee 100644 (file)
@@ -271,6 +271,11 @@ Handle<JSValue> GetScriptWrapper(Handle<Script> script);
 
 // Script line number computations.
 void InitScriptLineEnds(Handle<Script> script);
+// For string calculates an array of line end positions. If the string
+// does not end with a new line character, this character may optionally be
+// imagined.
+Handle<FixedArray> CalculateLineEnds(Handle<String> string,
+                                     bool with_imaginary_last_new_line);
 int GetScriptLineNumber(Handle<Script> script, int code_position);
 // The safe version does not make heap allocations but may work much slower.
 int GetScriptLineNumberSafe(Handle<Script> script, int code_position);
index 2b1abec..d2aee87 100644 (file)
@@ -36,19 +36,19 @@ Debug.LiveEdit = new function() {
   // being replaced with a completely different string new_str.
   //
   // Only one function will have its Code changed in result of this function.
-  // All nested functions (should they have any instances at the moment) are left
-  // unchanged and re-linked to a newly created script instance representing old
-  // version of the source. (Generally speaking,
+  // All nested functions (should they have any instances at the moment) are
+  // left unchanged and re-linked to a newly created script instance
+  // representing old version of the source. (Generally speaking,
   // during the change all nested functions are erased and completely different
   // set of nested functions are introduced.) All other functions just have
   // their positions updated.
   //
   // @param {Script} script that is being changed
-  // @param {Array} change_log a list that collects engineer-readable description
-  //     of what happened.
+  // @param {Array} change_log a list that collects engineer-readable
+  //     description of what happened.
   function ApplyPatch(script, change_pos, change_len, new_str,
       change_log) {
-  
+
     // Fully compiles source string as a script. Returns Array of
     // FunctionCompileInfo -- a descriptions of all functions of the script.
     // Elements of array are ordered by start positions of functions (from top
@@ -58,8 +58,8 @@ Debug.LiveEdit = new function() {
     // The script is used for compilation, because it produces code that
     // needs to be linked with some particular script (for nested functions).
     function DebugGatherCompileInfo(source) {
-      // Get function info, elements are partially sorted (it is a tree
-      // of nested functions serialized as parent followed by serialized children.
+      // Get function info, elements are partially sorted (it is a tree of
+      // nested functions serialized as parent followed by serialized children.
       var raw_compile_info = %LiveEditGatherCompileInfo(script, source);
   
       // Sort function infos by start position field.
@@ -117,7 +117,8 @@ Debug.LiveEdit = new function() {
       return compile_info;
     }
   
-    // Given a positions, finds a function that fully includes the entire change.
+    // Given a positions, finds a function that fully includes the entire
+    // change.
     function FindChangedFunction(compile_info, offset, len) {
       // First condition: function should start before the change region.
       // Function #0 (whole-script function) always does, but we want
@@ -269,7 +270,8 @@ Debug.LiveEdit = new function() {
   
     // Update the script text and create a new script representing an old
     // version of the script.
-    var old_script = %LiveEditReplaceScript(script, new_source, old_script_name);
+    var old_script = %LiveEditReplaceScript(script, new_source,
+        old_script_name);
   
     PatchCode(new_compile_info[function_being_patched],
         FindFunctionInfo(function_being_patched));
@@ -477,6 +479,12 @@ Debug.LiveEdit = new function() {
   }
   // Function is public.
   this.SetScriptSource = SetScriptSource;
+  
+  function CompareStringsLinewise(s1, s2) {
+    return %LiveEditCompareStringsLinewise(s1, s2);
+  }
+  // Function is public (for tests).
+  this.CompareStringsLinewise = CompareStringsLinewise;
 
   
   // Finds a difference between 2 strings in form of a single chunk.
index 8c1316b..4b30b2a 100644 (file)
@@ -42,6 +42,358 @@ namespace internal {
 
 #ifdef ENABLE_DEBUGGER_SUPPORT
 
+
+// A simple implementation of dynamic programming algorithm. It solves
+// the problem of finding the difference of 2 arrays. It uses a table of results
+// of subproblems. Each cell contains a number together with 2-bit flag
+// that helps building the chunk list.
+class Differencer {
+ public:
+  explicit Differencer(Compare::Input* input)
+      : input_(input), len1_(input->getLength1()), len2_(input->getLength2()) {
+    buffer_ = NewArray<int>(len1_ * len2_);
+  }
+  ~Differencer() {
+    DeleteArray(buffer_);
+  }
+
+  void Initialize() {
+    int array_size = len1_ * len2_;
+    for (int i = 0; i < array_size; i++) {
+      buffer_[i] = kEmptyCellValue;
+    }
+  }
+
+  // Makes sure that result for the full problem is calculated and stored
+  // in the table together with flags showing a path through subproblems.
+  void FillTable() {
+    CompareUpToTail(0, 0);
+  }
+
+  void SaveResult(Compare::Output* chunk_writer) {
+    ResultWriter writer(chunk_writer);
+
+    int pos1 = 0;
+    int pos2 = 0;
+    while (true) {
+      if (pos1 < len1_) {
+        if (pos2 < len2_) {
+          Direction dir = get_direction(pos1, pos2);
+          switch (dir) {
+            case EQ:
+              writer.eq();
+              pos1++;
+              pos2++;
+              break;
+            case SKIP1:
+              writer.skip1(1);
+              pos1++;
+              break;
+            case SKIP2:
+            case SKIP_ANY:
+              writer.skip2(1);
+              pos2++;
+              break;
+            default:
+              UNREACHABLE();
+          }
+        } else {
+          writer.skip1(len1_ - pos1);
+          break;
+        }
+      } else {
+        if (len2_ != pos2) {
+          writer.skip2(len2_ - pos2);
+        }
+        break;
+      }
+    }
+    writer.close();
+  }
+
+ private:
+  Compare::Input* input_;
+  int* buffer_;
+  int len1_;
+  int len2_;
+
+  enum Direction {
+    EQ = 0,
+    SKIP1,
+    SKIP2,
+    SKIP_ANY,
+
+    MAX_DIRECTION_FLAG_VALUE = SKIP_ANY
+  };
+
+  // Computes result for a subtask and optionally caches it in the buffer table.
+  // All results values are shifted to make space for flags in the lower bits.
+  int CompareUpToTail(int pos1, int pos2) {
+    if (pos1 < len1_) {
+      if (pos2 < len2_) {
+        int cached_res = get_value4(pos1, pos2);
+        if (cached_res == kEmptyCellValue) {
+          Direction dir;
+          int res;
+          if (input_->equals(pos1, pos2)) {
+            res = CompareUpToTail(pos1 + 1, pos2 + 1);
+            dir = EQ;
+          } else {
+            int res1 = CompareUpToTail(pos1 + 1, pos2) +
+                (1 << kDirectionSizeBits);
+            int res2 = CompareUpToTail(pos1, pos2 + 1) +
+                (1 << kDirectionSizeBits);
+            if (res1 == res2) {
+              res = res1;
+              dir = SKIP_ANY;
+            } else if (res1 < res2) {
+              res = res1;
+              dir = SKIP1;
+            } else {
+              res = res2;
+              dir = SKIP2;
+            }
+          }
+          set_value4_and_dir(pos1, pos2, res, dir);
+          cached_res = res;
+        }
+        return cached_res;
+      } else {
+        return (len1_ - pos1) << kDirectionSizeBits;
+      }
+    } else {
+      return (len2_ - pos2) << kDirectionSizeBits;
+    }
+  }
+
+  inline int& get_cell(int i1, int i2) {
+    return buffer_[i1 + i2 * len1_];
+  }
+
+  // Each cell keeps a value plus direction. Value is multiplied by 4.
+  void set_value4_and_dir(int i1, int i2, int value4, Direction dir) {
+    ASSERT((value4 & kDirectionMask) == 0);
+    get_cell(i1, i2) = value4 | dir;
+  }
+
+  int get_value4(int i1, int i2) {
+    return get_cell(i1, i2) & (kMaxUInt32 ^ kDirectionMask);
+  }
+  Direction get_direction(int i1, int i2) {
+    return static_cast<Direction>(get_cell(i1, i2) & kDirectionMask);
+  }
+
+  static const int kDirectionSizeBits = 2;
+  static const int kDirectionMask = (1 << kDirectionSizeBits) - 1;
+  static const int kEmptyCellValue = -1 << kDirectionSizeBits;
+
+  // This method only holds static assert statement (unfortunately you cannot
+  // place one in class scope).
+  void StaticAssertHolder() {
+    STATIC_ASSERT(MAX_DIRECTION_FLAG_VALUE < (1 << kDirectionSizeBits));
+  }
+
+  class ResultWriter {
+   public:
+    explicit ResultWriter(Compare::Output* chunk_writer)
+        : chunk_writer_(chunk_writer), pos1_(0), pos2_(0),
+          pos1_begin_(-1), pos2_begin_(-1), has_open_chunk_(false) {
+    }
+    void eq() {
+      FlushChunk();
+      pos1_++;
+      pos2_++;
+    }
+    void skip1(int len1) {
+      StartChunk();
+      pos1_ += len1;
+    }
+    void skip2(int len2) {
+      StartChunk();
+      pos2_ += len2;
+    }
+    void close() {
+      FlushChunk();
+    }
+
+   private:
+    Compare::Output* chunk_writer_;
+    int pos1_;
+    int pos2_;
+    int pos1_begin_;
+    int pos2_begin_;
+    bool has_open_chunk_;
+
+    void StartChunk() {
+      if (!has_open_chunk_) {
+        pos1_begin_ = pos1_;
+        pos2_begin_ = pos2_;
+        has_open_chunk_ = true;
+      }
+    }
+
+    void FlushChunk() {
+      if (has_open_chunk_) {
+        chunk_writer_->AddChunk(pos1_begin_, pos2_begin_,
+                                pos1_ - pos1_begin_, pos2_ - pos2_begin_);
+        has_open_chunk_ = false;
+      }
+    }
+  };
+};
+
+
+void Compare::CalculateDifference(Compare::Input* input,
+                                  Compare::Output* result_writer) {
+  Differencer differencer(input);
+  differencer.Initialize();
+  differencer.FillTable();
+  differencer.SaveResult(result_writer);
+}
+
+
+static bool CompareSubstrings(Handle<String> s1, int pos1,
+                              Handle<String> s2, int pos2, int len) {
+  static StringInputBuffer buf1;
+  static StringInputBuffer buf2;
+  buf1.Reset(*s1);
+  buf1.Seek(pos1);
+  buf2.Reset(*s2);
+  buf2.Seek(pos2);
+  for (int i = 0; i < len; i++) {
+    ASSERT(buf1.has_more() && buf2.has_more());
+    if (buf1.GetNext() != buf2.GetNext()) {
+      return false;
+    }
+  }
+  return true;
+}
+
+
+// Wraps raw n-elements line_ends array as a list of n+1 lines. The last line
+// never has terminating new line character.
+class LineEndsWrapper {
+ public:
+  explicit LineEndsWrapper(Handle<String> string)
+      : ends_array_(CalculateLineEnds(string, false)),
+        string_len_(string->length()) {
+  }
+  int length() {
+    return ends_array_->length() + 1;
+  }
+  // Returns start for any line including start of the imaginary line after
+  // the last line.
+  int GetLineStart(int index) {
+    if (index == 0) {
+      return 0;
+    } else {
+      return GetLineEnd(index - 1);
+    }
+  }
+  int GetLineEnd(int index) {
+    if (index == ends_array_->length()) {
+      // End of the last line is always an end of the whole string.
+      // If the string ends with a new line character, the last line is an
+      // empty string after this character.
+      return string_len_;
+    } else {
+      return GetPosAfterNewLine(index);
+    }
+  }
+
+ private:
+  Handle<FixedArray> ends_array_;
+  int string_len_;
+
+  int GetPosAfterNewLine(int index) {
+    return Smi::cast(ends_array_->get(index))->value() + 1;
+  }
+};
+
+
+// Represents 2 strings as 2 arrays of lines.
+class LineArrayCompareInput : public Compare::Input {
+ public:
+  LineArrayCompareInput(Handle<String> s1, Handle<String> s2,
+                        LineEndsWrapper line_ends1, LineEndsWrapper line_ends2)
+      : s1_(s1), s2_(s2), line_ends1_(line_ends1), line_ends2_(line_ends2) {
+  }
+  int getLength1() {
+    return line_ends1_.length();
+  }
+  int getLength2() {
+    return line_ends2_.length();
+  }
+  bool equals(int index1, int index2) {
+    int line_start1 = line_ends1_.GetLineStart(index1);
+    int line_start2 = line_ends2_.GetLineStart(index2);
+    int line_end1 = line_ends1_.GetLineEnd(index1);
+    int line_end2 = line_ends2_.GetLineEnd(index2);
+    int len1 = line_end1 - line_start1;
+    int len2 = line_end2 - line_start2;
+    if (len1 != len2) {
+      return false;
+    }
+    return CompareSubstrings(s1_, line_start1, s2_, line_start2, len1);
+  }
+
+ private:
+  Handle<String> s1_;
+  Handle<String> s2_;
+  LineEndsWrapper line_ends1_;
+  LineEndsWrapper line_ends2_;
+};
+
+
+// Stores compare result in JSArray. Each chunk is stored as 3 array elements:
+// (pos1, len1, len2).
+class LineArrayCompareOutput : public Compare::Output {
+ public:
+  LineArrayCompareOutput(LineEndsWrapper line_ends1, LineEndsWrapper line_ends2)
+      : array_(Factory::NewJSArray(10)), current_size_(0),
+        line_ends1_(line_ends1), line_ends2_(line_ends2) {
+  }
+
+  void AddChunk(int line_pos1, int line_pos2, int line_len1, int line_len2) {
+    int char_pos1 = line_ends1_.GetLineStart(line_pos1);
+    int char_pos2 = line_ends2_.GetLineStart(line_pos2);
+    int char_len1 = line_ends1_.GetLineStart(line_pos1 + line_len1) - char_pos1;
+    int char_len2 = line_ends2_.GetLineStart(line_pos2 + line_len2) - char_pos2;
+
+    SetElement(array_, current_size_, Handle<Object>(Smi::FromInt(char_pos1)));
+    SetElement(array_, current_size_ + 1,
+               Handle<Object>(Smi::FromInt(char_len1)));
+    SetElement(array_, current_size_ + 2,
+               Handle<Object>(Smi::FromInt(char_len2)));
+    current_size_ += 3;
+  }
+
+  Handle<JSArray> GetResult() {
+    return array_;
+  }
+
+ private:
+  Handle<JSArray> array_;
+  int current_size_;
+  LineEndsWrapper line_ends1_;
+  LineEndsWrapper line_ends2_;
+};
+
+
+Handle<JSArray> LiveEdit::CompareStringsLinewise(Handle<String> s1,
+                                                 Handle<String> s2) {
+  LineEndsWrapper line_ends1(s1);
+  LineEndsWrapper line_ends2(s2);
+
+  LineArrayCompareInput input(s1, s2, line_ends1, line_ends2);
+  LineArrayCompareOutput output(line_ends1, line_ends2);
+
+  Compare::CalculateDifference(&input, &output);
+
+  return output.GetResult();
+}
+
+
 static void CompileScriptForTracker(Handle<Script> script) {
   const bool is_eval = false;
   const bool is_global = true;
index a81bacf..5c73313 100644 (file)
@@ -109,6 +109,44 @@ class LiveEdit : AllStatic {
     FUNCTION_BLOCKED_UNDER_NATIVE_CODE = 4,
     FUNCTION_REPLACED_ON_ACTIVE_STACK = 5
   };
+
+  // Compares 2 strings line-by-line and returns diff in form of array of
+  // triplets (pos1, len1, len2) describing list of diff chunks.
+  static Handle<JSArray> CompareStringsLinewise(Handle<String> s1,
+                                                Handle<String> s2);
+};
+
+
+// A general-purpose comparator between 2 arrays.
+class Compare {
+ public:
+
+  // Holds 2 arrays of some elements allowing to compare any pair of
+  // element from the first array and element from the second array.
+  class Input {
+   public:
+    virtual int getLength1() = 0;
+    virtual int getLength2() = 0;
+    virtual bool equals(int index1, int index2) = 0;
+
+   protected:
+    virtual ~Input() {}
+  };
+
+  // Receives compare result as a series of chunks.
+  class Output {
+   public:
+    // Puts another chunk in result list. Note that technically speaking
+    // only 3 arguments actually needed with 4th being derivable.
+    virtual void AddChunk(int pos1, int pos2, int len1, int len2) = 0;
+
+   protected:
+    virtual ~Output() {}
+  };
+
+  // Finds the difference between 2 arrays of elements.
+  static void CalculateDifference(Input* input,
+                                  Output* result_writer);
 };
 
 #endif  // ENABLE_DEBUGGER_SUPPORT
index 4146366..67d60e1 100644 (file)
@@ -9756,10 +9756,21 @@ static Object* Runtime_LiveEditCheckAndDropActivations(Arguments args) {
   CONVERT_ARG_CHECKED(JSArray, shared_array, 0);
   CONVERT_BOOLEAN_CHECKED(do_drop, args[1]);
 
-
   return *LiveEdit::CheckAndDropActivations(shared_array, do_drop);
 }
 
+// Compares 2 strings line-by-line and returns diff in form of JSArray of
+// triplets (pos1, len1, len2) describing list of diff chunks.
+static Object* Runtime_LiveEditCompareStringsLinewise(Arguments args) {
+  ASSERT(args.length() == 2);
+  HandleScope scope;
+  CONVERT_ARG_CHECKED(String, s1, 0);
+  CONVERT_ARG_CHECKED(String, s2, 1);
+
+  return *LiveEdit::CompareStringsLinewise(s1, s2);
+}
+
+
 
 // A testing entry. Returns statement position which is the closest to
 // source_position.
index df0d16e..07f51e4 100644 (file)
@@ -340,6 +340,7 @@ namespace internal {
   F(LiveEditRelinkFunctionToScript, 2, 1) \
   F(LiveEditPatchFunctionPositions, 2, 1) \
   F(LiveEditCheckAndDropActivations, 2, 1) \
+  F(LiveEditCompareStringsLinewise, 2, 1) \
   F(GetFunctionCodePositionFromSource, 2, 1) \
   F(ExecuteInDebugContext, 2, 1)
 #else
index 9c19c2b..5eab892 100644 (file)
@@ -55,6 +55,7 @@ SOURCES = {
     'test-heap.cc',
     'test-heap-profiler.cc',
     'test-list.cc',
+    'test-liveedit.cc',
     'test-lock.cc',
     'test-log.cc',
     'test-log-utils.cc',
diff --git a/test/cctest/test-liveedit.cc b/test/cctest/test-liveedit.cc
new file mode 100644 (file)
index 0000000..32df906
--- /dev/null
@@ -0,0 +1,174 @@
+// Copyright 2007-2008 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+//       copyright notice, this list of conditions and the following
+//       disclaimer in the documentation and/or other materials provided
+//       with the distribution.
+//     * Neither the name of Google Inc. nor the names of its
+//       contributors may be used to endorse or promote products derived
+//       from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <stdlib.h>
+
+#include "v8.h"
+
+#include "liveedit.h"
+#include "cctest.h"
+
+
+using namespace v8::internal;
+
+// Anonymous namespace.
+namespace {
+
+class StringCompareInput : public Compare::Input {
+ public:
+  StringCompareInput(const char* s1, const char* s2) : s1_(s1), s2_(s2) {
+  }
+  int getLength1() {
+    return strlen(s1_);
+  }
+  int getLength2() {
+    return strlen(s2_);
+  }
+  bool equals(int index1, int index2) {
+    return s1_[index1] == s2_[index2];
+  }
+
+ private:
+  const char* s1_;
+  const char* s2_;
+};
+
+
+class DiffChunkStruct : public ZoneObject {
+ public:
+  DiffChunkStruct(int pos1_param, int pos2_param,
+                  int len1_param, int len2_param)
+      : pos1(pos1_param), pos2(pos2_param),
+        len1(len1_param), len2(len2_param), next(NULL) {}
+  int pos1;
+  int pos2;
+  int len1;
+  int len2;
+  DiffChunkStruct* next;
+};
+
+
+class ListDiffOutputWriter : public Compare::Output {
+ public:
+  explicit ListDiffOutputWriter(DiffChunkStruct** next_chunk_pointer)
+      : next_chunk_pointer_(next_chunk_pointer) {
+    (*next_chunk_pointer_) = NULL;
+  }
+  void AddChunk(int pos1, int pos2, int len1, int len2) {
+    current_chunk_ = new DiffChunkStruct(pos1, pos2, len1, len2);
+    (*next_chunk_pointer_) = current_chunk_;
+    next_chunk_pointer_ = &current_chunk_->next;
+  }
+ private:
+  DiffChunkStruct** next_chunk_pointer_;
+  DiffChunkStruct* current_chunk_;
+};
+
+
+void CompareStringsOneWay(const char* s1, const char* s2,
+                          int expected_diff_parameter = -1) {
+  StringCompareInput input(s1, s2);
+
+  ZoneScope zone_scope(DELETE_ON_EXIT);
+
+  DiffChunkStruct* first_chunk;
+  ListDiffOutputWriter writer(&first_chunk);
+
+  Compare::CalculateDifference(&input, &writer);
+
+  int len1 = strlen(s1);
+  int len2 = strlen(s2);
+
+  int pos1 = 0;
+  int pos2 = 0;
+
+  int diff_parameter = 0;
+
+  for (DiffChunkStruct* chunk = first_chunk;
+      chunk != NULL;
+      chunk = chunk->next) {
+    int diff_pos1 = chunk->pos1;
+    int similar_part_length = diff_pos1 - pos1;
+    int diff_pos2 = pos2 + similar_part_length;
+
+    ASSERT_EQ(diff_pos2, chunk->pos2);
+
+    for (int j = 0; j < similar_part_length; j++) {
+      ASSERT(pos1 + j < len1);
+      ASSERT(pos2 + j < len2);
+      ASSERT_EQ(s1[pos1 + j], s2[pos2 + j]);
+    }
+    diff_parameter += chunk->len1 + chunk->len2;
+    pos1 = diff_pos1 + chunk->len1;
+    pos2 = diff_pos2 + chunk->len2;
+  }
+  {
+    // After last chunk.
+    int similar_part_length = len1 - pos1;
+    ASSERT_EQ(similar_part_length, len2 - pos2);
+    USE(len2);
+    for (int j = 0; j < similar_part_length; j++) {
+      ASSERT(pos1 + j < len1);
+      ASSERT(pos2 + j < len2);
+      ASSERT_EQ(s1[pos1 + j], s2[pos2 + j]);
+    }
+  }
+
+  if (expected_diff_parameter != -1) {
+    ASSERT_EQ(expected_diff_parameter, diff_parameter);
+  }
+}
+
+
+void CompareStrings(const char* s1, const char* s2,
+                    int expected_diff_parameter = -1) {
+  CompareStringsOneWay(s1, s2, expected_diff_parameter);
+  CompareStringsOneWay(s2, s1, expected_diff_parameter);
+}
+
+}  // Anonymous namespace.
+
+
+// --- T h e   A c t u a l   T e s t s
+
+TEST(LiveEditDiffer) {
+  CompareStrings("zz1zzz12zz123zzz", "zzzzzzzzzz", 6);
+  CompareStrings("zz1zzz12zz123zzz", "zz0zzz0zz0zzz", 9);
+  CompareStrings("123456789", "987654321", 16);
+  CompareStrings("zzz", "yyy", 6);
+  CompareStrings("zzz", "zzz12", 2);
+  CompareStrings("zzz", "21zzz", 2);
+  CompareStrings("cat", "cut", 2);
+  CompareStrings("ct", "cut", 1);
+  CompareStrings("cat", "ct", 1);
+  CompareStrings("cat", "cat", 0);
+  CompareStrings("", "", 0);
+  CompareStrings("cat", "", 3);
+  CompareStrings("a cat", "a capybara", 7);
+  CompareStrings("abbabababababaaabbabababababbabbbbbbbababa",
+                 "bbbbabababbbabababbbabababababbabbababa");
+}
diff --git a/test/mjsunit/debug-liveedit-diff.js b/test/mjsunit/debug-liveedit-diff.js
new file mode 100644 (file)
index 0000000..402bf14
--- /dev/null
@@ -0,0 +1,100 @@
+// Copyright 2010 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+//       copyright notice, this list of conditions and the following
+//       disclaimer in the documentation and/or other materials provided
+//       with the distribution.
+//     * Neither the name of Google Inc. nor the names of its
+//       contributors may be used to endorse or promote products derived
+//       from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Flags: --expose-debug-as debug
+// Get the Debug object exposed from the debug context global object.
+
+Debug = debug.Debug
+
+function CheckCompareOneWay(s1, s2) {
+  var diff_array = Debug.LiveEdit.CompareStringsLinewise(s1, s2);
+
+  var pos1 = 0;
+  var pos2 = 0;
+  print("Compare:");
+  for (var i = 0; i < diff_array.length; i += 3) {
+    var similar_length = diff_array[i] - pos1;
+    assertEquals(s1.substring(pos1, pos1 + similar_length),
+                 s2.substring(pos2, pos2 + similar_length));
+
+    print(s1.substring(pos1, pos1 + similar_length));
+    pos1 += similar_length;
+    pos2 += similar_length;
+    print("<<< " + pos1 + " " + diff_array[i + 1]);
+    print(s1.substring(pos1, pos1 + diff_array[i + 1]));
+    print("===");
+    print(s2.substring(pos2, pos2 + diff_array[i + 2]));
+    print(">>> " + pos2 + " " + diff_array[i + 2]);
+    pos1 += diff_array[i + 1];
+    pos2 += diff_array[i + 2];
+  }
+  {
+    // After last change
+    var similar_length = s1.length - pos1;
+    assertEquals(similar_length, s2.length - pos2);
+    assertEquals(s1.substring(pos1, pos1 + similar_length),
+                 s2.substring(pos2, pos2 + similar_length));
+
+    print(s1.substring(pos1, pos1 + similar_length));
+  }
+  print("");
+}
+
+function CheckCompare(s1, s2) {
+  CheckCompareOneWay(s1, s2);
+  CheckCompareOneWay(s2, s1);
+}
+
+CheckCompare("", "");
+
+CheckCompare("a", "b");
+
+CheckCompare(
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway",
+    "yesterday\nall\nmy\ntroubles\nseem\nso\nfar\naway"
+);
+
+CheckCompare(
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway",
+    "\nall\nmy\ntroubles\nseemed\nso\nfar\naway"
+);
+
+CheckCompare(
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway",
+    "all\nmy\ntroubles\nseemed\nso\nfar\naway"
+);
+
+CheckCompare(
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway",
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway\n"
+);
+
+CheckCompare(
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\nfar\naway",
+    "yesterday\nall\nmy\ntroubles\nseemed\nso\n"
+);
+