LiveEdit: patch positions in function
authorpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Mon, 15 Mar 2010 21:06:51 +0000 (21:06 +0000)
committerpeter.rybin@gmail.com <peter.rybin@gmail.com@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Mon, 15 Mar 2010 21:06:51 +0000 (21:06 +0000)
Review URL: http://codereview.chromium.org/914003

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

src/factory.cc
src/factory.h
src/heap.cc
src/heap.h
src/liveedit-delay.js
src/liveedit.cc
src/runtime.cc
src/runtime.h
test/mjsunit/debug-liveedit-patch-positions-replace.js [new file with mode: 0644]
test/mjsunit/debug-liveedit-patch-positions.js [new file with mode: 0644]

index 993e2b6ce9da855f1bceea4a70d36e0d46bf0f34..45124e645197c12d04055411fdbe6d67f5608f3a 100644 (file)
@@ -560,6 +560,11 @@ Handle<Code> Factory::CopyCode(Handle<Code> code) {
 }
 
 
+Handle<Code> Factory::CopyCode(Handle<Code> code, Vector<byte> reloc_info) {
+  CALL_HEAP_FUNCTION(Heap::CopyCode(*code, reloc_info), Code);
+}
+
+
 static inline Object* DoCopyInsert(DescriptorArray* array,
                                    String* key,
                                    Object* value,
index e08bde41ad0b60595434c2bc2b6d489477bc1aad..cc96e11a89b3ebd23ca4ba00d246fcaf66748692 100644 (file)
@@ -230,6 +230,8 @@ class Factory : public AllStatic {
 
   static Handle<Code> CopyCode(Handle<Code> code);
 
+  static Handle<Code> CopyCode(Handle<Code> code, Vector<byte> reloc_info);
+
   static Handle<Object> ToObject(Handle<Object> object);
   static Handle<Object> ToObject(Handle<Object> object,
                                  Handle<Context> global_context);
index d6668754220c9354cdc2c7a06fe82c2a39717691..08a5db8ed6e9a92a96972c113c816c079b5c363e 100644 (file)
@@ -2257,6 +2257,55 @@ Object* Heap::CopyCode(Code* code) {
 }
 
 
+Object* Heap::CopyCode(Code* code, Vector<byte> reloc_info) {
+  int new_body_size = RoundUp(code->instruction_size() + reloc_info.length(),
+                              kObjectAlignment);
+
+  int sinfo_size = code->sinfo_size();
+
+  int new_obj_size = Code::SizeFor(new_body_size, sinfo_size);
+
+  Address old_addr = code->address();
+
+  int relocation_offset = code->relocation_start() - old_addr;
+
+  Object* result;
+  if (new_obj_size > MaxObjectSizeInPagedSpace()) {
+    result = lo_space_->AllocateRawCode(new_obj_size);
+  } else {
+    result = code_space_->AllocateRaw(new_obj_size);
+  }
+
+  if (result->IsFailure()) return result;
+
+  // Copy code object.
+  Address new_addr = reinterpret_cast<HeapObject*>(result)->address();
+
+  // Copy header and instructions.
+  memcpy(new_addr, old_addr, relocation_offset);
+
+  // Copy patched rinfo.
+  memcpy(new_addr + relocation_offset,
+         reloc_info.start(),
+             reloc_info.length());
+
+  Code* new_code = Code::cast(result);
+  new_code->set_relocation_size(reloc_info.length());
+
+  // Copy sinfo.
+  memcpy(new_code->sinfo_start(), code->sinfo_start(), code->sinfo_size());
+
+  // Relocate the copy.
+  ASSERT(!CodeRange::exists() || CodeRange::contains(code->address()));
+  new_code->Relocate(new_addr - old_addr);
+
+#ifdef DEBUG
+  code->Verify();
+#endif
+  return new_code;
+}
+
+
 Object* Heap::Allocate(Map* map, AllocationSpace space) {
   ASSERT(gc_state_ == NOT_IN_GC);
   ASSERT(map->instance_type() != MAP_TYPE);
index eee5a0577f5dd8fedea500c1c39da00ee39a144d..1bcd4218ceadf15aeb7105293e30d49eb2f5fcf6 100644 (file)
@@ -612,6 +612,11 @@ class Heap : public AllStatic {
                             Handle<Object> self_reference);
 
   static Object* CopyCode(Code* code);
+
+  // Copy the code and scope info part of the code object, but insert
+  // the provided data as the relocation information.
+  static Object* CopyCode(Code* code, Vector<byte> reloc_info);
+
   // Finds the symbol for string in the symbol table.
   // If not found, a new symbol is added to the table and returned.
   // Returns Failure::RetryAfterGC(requested_bytes, space) if allocation
index 12479b13c819e7c62b06a6e62bf38441c648d488..41f894a2135b2c9adbb72995cf94b14339cdf8e0 100644 (file)
@@ -424,3 +424,8 @@ Debug.LiveEditChangeScript.Failure = function(message) {
 Debug.LiveEditChangeScript.Failure.prototype.toString = function() {
   return "LiveEdit Failure: " + this.message;     
 }
+
+// A testing entry.
+Debug.LiveEditChangeScript.GetPcFromSourcePos = function(func, source_pos) {
+  return %GetFunctionCodePositionFromSource(func, source_pos);
+}
index 513cc0233f18de5464080317b4a80027d1c6bdbd..4533f9c7ddab854446ab33ece7b4ed93685d2587 100644 (file)
@@ -346,8 +346,82 @@ void LiveEdit::WrapSharedFunctionInfos(Handle<JSArray> array) {
 }
 
 
+// Visitor that collects all references to a particular code object,
+// including "CODE_TARGET" references in other code objects.
+// It works in context of ZoneScope.
+class ReferenceCollectorVisitor : public ObjectVisitor {
+ public:
+  explicit ReferenceCollectorVisitor(Code* original)
+      : original_(original), rvalues_(10), reloc_infos_(10) {
+  }
+
+  virtual void VisitPointers(Object** start, Object** end) {
+    for (Object** p = start; p < end; p++) {
+      if (*p == original_) {
+        rvalues_.Add(p);
+      }
+    }
+  }
+
+  void VisitCodeTarget(RelocInfo* rinfo) {
+    ASSERT(RelocInfo::IsCodeTarget(rinfo->rmode()));
+    if (Code::GetCodeFromTargetAddress(rinfo->target_address()) == original_) {
+      reloc_infos_.Add(*rinfo);
+    }
+  }
+
+  virtual void VisitDebugTarget(RelocInfo* rinfo) {
+    VisitCodeTarget(rinfo);
+  }
+
+  // Post-visiting method that iterates over all collected references and
+  // modifies them.
+  void Replace(Code* substitution) {
+    for (int i = 0; i < rvalues_.length(); i++) {
+      *(rvalues_[i]) = substitution;
+    }
+    for (int i = 0; i < reloc_infos_.length(); i++) {
+      reloc_infos_[i].set_target_address(substitution->instruction_start());
+    }
+  }
+
+ private:
+  Code* original_;
+  ZoneList<Object**> rvalues_;
+  ZoneList<RelocInfo> reloc_infos_;
+};
+
+// Finds all references to original and replaces them with substitution.
+static void ReplaceCodeObject(Code* original, Code* substitution) {
+  ASSERT(!Heap::InNewSpace(substitution));
+
+  AssertNoAllocation no_allocations_please;
+
+  // A zone scope for ReferenceCollectorVisitor.
+  ZoneScope scope(DELETE_ON_EXIT);
+
+  ReferenceCollectorVisitor visitor(original);
+
+  // Iterate over all roots. Stack frames may have pointer into original code,
+  // so temporary replace the pointers with offset numbers
+  // in prologue/epilogue.
+  ThreadManager::MarkCompactPrologue(true);
+  Heap::IterateStrongRoots(&visitor, VISIT_ALL);
+  ThreadManager::MarkCompactEpilogue(true);
+
+  // Now iterate over all pointers of all objects, including code_target
+  // implicit pointers.
+  HeapIterator iterator;
+  for (HeapObject* obj = iterator.next(); obj != NULL; obj = iterator.next()) {
+    obj->Iterate(&visitor);
+  }
+
+  visitor.Replace(substitution);
+}
+
+
 void LiveEdit::ReplaceFunctionCode(Handle<JSArray> new_compile_info_array,
-                                 Handle<JSArray> shared_info_array) {
+                                   Handle<JSArray> shared_info_array) {
   HandleScope scope;
 
   FunctionInfoWrapper compile_info_wrapper(new_compile_info_array);
@@ -355,8 +429,9 @@ void LiveEdit::ReplaceFunctionCode(Handle<JSArray> new_compile_info_array,
 
   Handle<SharedFunctionInfo> shared_info = shared_info_wrapper.GetInfo();
 
-  shared_info->set_code(*(compile_info_wrapper.GetFunctionCode()),
-                        UPDATE_WRITE_BARRIER);
+  ReplaceCodeObject(shared_info->code(),
+                       *(compile_info_wrapper.GetFunctionCode()));
+
   shared_info->set_start_position(compile_info_wrapper.GetStartPosition());
   shared_info->set_end_position(compile_info_wrapper.GetEndPosition());
   // update breakpoints, original code, constructor stub
@@ -389,22 +464,137 @@ static int TranslatePosition(int original_position,
   for (int i = 0; i < array_len; i += 3) {
     int chunk_start =
         Smi::cast(position_change_array->GetElement(i))->value();
-    int chunk_end =
-        Smi::cast(position_change_array->GetElement(i + 1))->value();
-    int chunk_changed_end =
-        Smi::cast(position_change_array->GetElement(i + 2))->value();
-    position_diff = chunk_changed_end - chunk_end;
     if (original_position < chunk_start) {
       break;
     }
+    int chunk_end =
+        Smi::cast(position_change_array->GetElement(i + 1))->value();
     // Position mustn't be inside a chunk.
     ASSERT(original_position >= chunk_end);
+    int chunk_changed_end =
+        Smi::cast(position_change_array->GetElement(i + 2))->value();
+    position_diff = chunk_changed_end - chunk_end;
   }
 
   return original_position + position_diff;
 }
 
 
+// Auto-growing buffer for writing relocation info code section. This buffer
+// is a simplified version of buffer from Assembler. Unlike Assembler, this
+// class is platform-independent and it works without dealing with instructions.
+// As specified by RelocInfo format, the buffer is filled in reversed order:
+// from upper to lower addresses.
+// It uses NewArray/DeleteArray for memory management.
+class RelocInfoBuffer {
+ public:
+  RelocInfoBuffer(int buffer_initial_capicity, byte* pc) {
+    buffer_size_ = buffer_initial_capicity + kBufferGap;
+    buffer_ = NewArray<byte>(buffer_size_);
+
+    reloc_info_writer_.Reposition(buffer_ + buffer_size_, pc);
+  }
+  ~RelocInfoBuffer() {
+    DeleteArray(buffer_);
+  }
+
+  // As specified by RelocInfo format, the buffer is filled in reversed order:
+  // from upper to lower addresses.
+  void Write(const RelocInfo* rinfo) {
+    if (buffer_ + kBufferGap >= reloc_info_writer_.pos()) {
+      Grow();
+    }
+    reloc_info_writer_.Write(rinfo);
+  }
+
+  Vector<byte> GetResult() {
+    // Return the bytes from pos up to end of buffer.
+    return Vector<byte>(reloc_info_writer_.pos(),
+                        buffer_ + buffer_size_ - reloc_info_writer_.pos());
+  }
+
+ private:
+  void Grow() {
+    // Compute new buffer size.
+    int new_buffer_size;
+    if (buffer_size_ < 2 * KB) {
+      new_buffer_size = 4 * KB;
+    } else {
+      new_buffer_size = 2 * buffer_size_;
+    }
+    // Some internal data structures overflow for very large buffers,
+    // they must ensure that kMaximalBufferSize is not too large.
+    if (new_buffer_size > kMaximalBufferSize) {
+      V8::FatalProcessOutOfMemory("RelocInfoBuffer::GrowBuffer");
+    }
+
+    // Setup new buffer.
+    byte* new_buffer = NewArray<byte>(new_buffer_size);
+
+    // Copy the data.
+    int curently_used_size = buffer_ + buffer_size_ - reloc_info_writer_.pos();
+    memmove(new_buffer + new_buffer_size - curently_used_size,
+            reloc_info_writer_.pos(), curently_used_size);
+
+    reloc_info_writer_.Reposition(
+        new_buffer + new_buffer_size - curently_used_size,
+        reloc_info_writer_.last_pc());
+
+    DeleteArray(buffer_);
+    buffer_ = new_buffer;
+    buffer_size_ = new_buffer_size;
+  }
+
+  RelocInfoWriter reloc_info_writer_;
+  byte* buffer_;
+  int buffer_size_;
+
+  static const int kBufferGap = 8;
+  static const int kMaximalBufferSize = 512*MB;
+};
+
+// Patch positions in code (changes relocation info section) and possibly
+// returns new instance of code.
+static Handle<Code> PatchPositionsInCode(Handle<Code> code,
+    Handle<JSArray> position_change_array) {
+
+  RelocInfoBuffer buffer_writer(code->relocation_size(),
+                                code->instruction_start());
+
+  {
+    AssertNoAllocation no_allocations_please;
+    for (RelocIterator it(*code); !it.done(); it.next()) {
+      RelocInfo* rinfo = it.rinfo();
+      if (RelocInfo::IsPosition(rinfo->rmode())) {
+        int position = static_cast<int>(rinfo->data());
+        int new_position = TranslatePosition(position,
+                                             position_change_array);
+        if (position != new_position) {
+          RelocInfo info_copy(rinfo->pc(), rinfo->rmode(), new_position);
+          buffer_writer.Write(&info_copy);
+          continue;
+        }
+      }
+      buffer_writer.Write(it.rinfo());
+    }
+  }
+
+  Vector<byte> buffer = buffer_writer.GetResult();
+
+  if (buffer.length() == code->relocation_size()) {
+    // Simply patch relocation area of code.
+    memcpy(code->relocation_start(), buffer.start(), buffer.length());
+    return code;
+  } else {
+    // Relocation info section now has different size. We cannot simply
+    // rewrite it inside code object. Instead we have to create a new
+    // code object.
+    Handle<Code> result(Factory::CopyCode(code, buffer));
+    return result;
+  }
+}
+
+
 void LiveEdit::PatchFunctionPositions(Handle<JSArray> shared_info_array,
                                       Handle<JSArray> position_change_array) {
   SharedInfoWrapper shared_info_wrapper(shared_info_array);
@@ -415,7 +605,45 @@ void LiveEdit::PatchFunctionPositions(Handle<JSArray> shared_info_array,
   info->set_end_position(TranslatePosition(info->end_position(),
                                            position_change_array));
 
-  // Also patch rinfos (both in working code and original code), breakpoints.
+  info->set_function_token_position(
+      TranslatePosition(info->function_token_position(),
+      position_change_array));
+
+  // Patch relocation info section of the code.
+  Handle<Code> patched_code = PatchPositionsInCode(Handle<Code>(info->code()),
+                                                   position_change_array);
+  if (*patched_code != info->code()) {
+    // Replace all references to the code across the heap. In particular,
+    // some stubs may refer to this code and this code may be being executed
+    // on stack (it is safe to substitute the code object on stack, because
+    // we only change the structure of rinfo and leave instructions untouched).
+    ReplaceCodeObject(info->code(), *patched_code);
+  }
+
+  if (info->debug_info()->IsDebugInfo()) {
+    Handle<DebugInfo> debug_info(DebugInfo::cast(info->debug_info()));
+    Handle<Code> patched_orig_code =
+        PatchPositionsInCode(Handle<Code>(debug_info->original_code()),
+                             position_change_array);
+    if (*patched_orig_code != debug_info->original_code()) {
+      // Do not use expensive ReplaceCodeObject for original_code, because we
+      // do not expect any other references except this one.
+      debug_info->set_original_code(*patched_orig_code);
+    }
+
+    Handle<FixedArray> break_point_infos(debug_info->break_points());
+    for (int i = 0; i < break_point_infos->length(); i++) {
+      if (!break_point_infos->get(i)->IsBreakPointInfo()) {
+        continue;
+      }
+      Handle<BreakPointInfo> info(
+          BreakPointInfo::cast(break_point_infos->get(i)));
+      int new_position = TranslatePosition(info->source_position()->value(),
+          position_change_array);
+      info->set_source_position(Smi::FromInt(new_position));
+    }
+  }
+  // TODO(635): Also patch breakpoint objects in JS.
 }
 
 
index 4a9b7e0ff420d2c744df7d23d516899e61d3e82b..fdea88ae2425cc6f42f03b8acd40a13ec51fea68 100644 (file)
@@ -9063,6 +9063,36 @@ static Object* Runtime_LiveEditCheckStackActivations(Arguments args) {
 }
 
 
+// A testing entry. Returns statement position which is the closest to
+// source_position.
+static Object* Runtime_GetFunctionCodePositionFromSource(Arguments args) {
+  ASSERT(args.length() == 2);
+  HandleScope scope;
+  CONVERT_ARG_CHECKED(JSFunction, function, 0);
+  CONVERT_NUMBER_CHECKED(int32_t, source_position, Int32, args[1]);
+
+  Handle<Code> code(function->code());
+
+  RelocIterator it(*code, 1 << RelocInfo::STATEMENT_POSITION);
+  int closest_pc = 0;
+  int distance = kMaxInt;
+  while (!it.done()) {
+    int statement_position = static_cast<int>(it.rinfo()->data());
+    // Check if this break point is closer that what was previously found.
+    if (source_position <= statement_position &&
+        statement_position - source_position < distance) {
+      closest_pc = it.rinfo()->pc() - code->instruction_start();
+      distance = statement_position - source_position;
+      // Check whether we can't get any closer.
+      if (distance == 0) break;
+    }
+    it.next();
+  }
+
+  return Smi::FromInt(closest_pc);
+}
+
+
 #endif  // ENABLE_DEBUGGER_SUPPORT
 
 #ifdef ENABLE_LOGGING_AND_PROFILING
index 71edc3a6e3eccbe139f4e7807c2aa4239b3b29ec..05f09f6f3ef87e3d4c8ea798c39f538096549ac6 100644 (file)
@@ -333,7 +333,8 @@ namespace internal {
   F(LiveEditReplaceFunctionCode, 2, 1) \
   F(LiveEditRelinkFunctionToScript, 2, 1) \
   F(LiveEditPatchFunctionPositions, 2, 1) \
-  F(LiveEditCheckStackActivations, 1, 1)
+  F(LiveEditCheckStackActivations, 1, 1) \
+  F(GetFunctionCodePositionFromSource, 2, 1)
 #else
 #define RUNTIME_FUNCTION_LIST_DEBUGGER_SUPPORT(F)
 #endif
diff --git a/test/mjsunit/debug-liveedit-patch-positions-replace.js b/test/mjsunit/debug-liveedit-patch-positions-replace.js
new file mode 100644 (file)
index 0000000..4b5fed7
--- /dev/null
@@ -0,0 +1,83 @@
+// 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.
+
+// Scenario: a function is being changed, which causes enclosing function to
+// have its positions patched; position changing requires new instance of Code
+// object to be introduced; the function happens to be on stack at this moment;
+// later it will resume over new instance of Code.
+// Before the change 2 rinfo are 22 characters away from each other. After the
+// change they are 114 characters away from each other. New instance of Code is
+// required when those numbers cross the border value of 64 (in any direction).
+
+Debug = debug.Debug
+
+eval(
+    "function BeingReplaced(changer, opt_x, opt_y) {\n" +
+    "  changer();\n" +
+    "  var res = new Object();\n" +
+    "  if (opt_x) { res.y = opt_y; }\n" +
+    "  res.a = (function() {})();\n" +
+    "  return res.a;\n" +
+    "}"
+);
+
+var script = Debug.findScript(BeingReplaced);
+
+var orig_body = "{}";
+var patch_pos = script.source.indexOf(orig_body);
+// Line long enough to change rinfo encoding.
+var new_body_patch = "{return 'Capybara';" +
+    "                                                                          " +
+    "}";
+
+var change_log = new Array();
+function Changer() {
+  Debug.LiveEditChangeScript(script, patch_pos, orig_body.length, new_body_patch, change_log);
+  print("Change log: " + JSON.stringify(change_log) + "\n");
+}
+
+function NoOp() {
+}
+
+function CallM(changer) {
+  // We expect call IC here after several function runs.
+  return BeingReplaced(changer);
+}
+
+// This several iterations should cause call IC for BeingReplaced call. This IC
+// will keep reference to code object of BeingRepalced function. This reference
+// should also be patched. Unfortunately, this is a manually checked fact (from
+// debugger or debug print) and doesn't work as an automatic test.
+CallM(NoOp);
+CallM(NoOp);
+CallM(NoOp);
+
+var res = CallM(Changer);
+assertEquals("Capybara", res);
diff --git a/test/mjsunit/debug-liveedit-patch-positions.js b/test/mjsunit/debug-liveedit-patch-positions.js
new file mode 100644 (file)
index 0000000..88ccda9
--- /dev/null
@@ -0,0 +1,93 @@
+// 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.
+
+// Scenario: some function is being edited; the outer function has to have its
+// positions patched. Accoring to a special markup of function text
+// corresponding byte-code PCs should conicide before change and after it.
+
+Debug = debug.Debug
+
+eval(
+    "function F1() {  return 5; }\n" +
+    "function ChooseAnimal(/*$*/ ) {\n" +
+    "/*$*/ var x = F1(/*$*/ );\n" +
+    "/*$*/ var res/*$*/  =/*$*/ (function() { return 'Cat'; } )();\n" +
+    "/*$*/ var y/*$*/  = F2(/*$*/ F1()/*$*/ , F1(/*$*/ )/*$*/ );\n" +
+    "/*$*/ if (/*$*/ x.toString(/*$*/ )) { /*$*/ y = 3;/*$*/  } else {/*$*/  y = 8;/*$*/  }\n" +
+    "/*$*/ var z = /*$*/ x * y;\n" +
+    "/*$*/ return/*$*/  res/*$*/  + z;/*$*/  }\n" +
+    "function F2(x, y) { return x + y; }"
+);
+
+// Find all *$* markers in text of the function and read corresponding statement
+// PCs.
+function ReadMarkerPositions(func) {
+  var text = func.toString();
+  var positions = new Array();
+  var match;
+  var pattern = /\/\*\$\*\//g;
+  while ((match = pattern.exec(text)) != null) {
+    positions.push(match.index);
+  }
+  return positions;
+}
+
+function ReadPCMap(func, positions) {
+  var res = new Array();
+  for (var i = 0; i < positions.length; i++) {
+    res.push(Debug.LiveEditChangeScript.GetPcFromSourcePos(func, positions[i]));
+  }
+  return res;
+}
+
+var res = ChooseAnimal();
+assertEquals("Cat15", res);
+
+var markerPositionsBefore = ReadMarkerPositions(ChooseAnimal);
+var pcArrayBefore = ReadPCMap(ChooseAnimal, markerPositionsBefore);
+
+var script = Debug.findScript(ChooseAnimal);
+
+var orig_animal = "'Cat'";
+var patch_pos = script.source.indexOf(orig_animal);
+var new_animal_patch = "'Capybara'";
+
+var change_log = new Array();
+Debug.LiveEditChangeScript(script, patch_pos, orig_animal.length, new_animal_patch, change_log);
+print("Change log: " + JSON.stringify(change_log) + "\n");
+
+var res = ChooseAnimal();
+assertEquals("Capybara15", res);
+
+var markerPositionsAfter = ReadMarkerPositions(ChooseAnimal);
+var pcArrayAfter = ReadPCMap(ChooseAnimal, markerPositionsAfter);
+
+assertArrayEquals(pcArrayBefore, pcArrayAfter);
+