1// Copyright 2012 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// LiveEdit feature implementation. The script should be executed after 6// debug-debugger.js. 7 8// A LiveEdit namespace. It contains functions that modifies JavaScript code 9// according to changes of script source (if possible). 10// 11// When new script source is put in, the difference is calculated textually, 12// in form of list of delete/add/change chunks. The functions that include 13// change chunk(s) get recompiled, or their enclosing functions are 14// recompiled instead. 15// If the function may not be recompiled (e.g. it was completely erased in new 16// version of the script) it remains unchanged, but the code that could 17// create a new instance of this function goes away. An old version of script 18// is created to back up this obsolete function. 19// All unchanged functions have their positions updated accordingly. 20// 21// LiveEdit namespace is declared inside a single function constructor. 22Debug.LiveEdit = new function() { 23 24 // Forward declaration for minifier. 25 var FunctionStatus; 26 27 var NEEDS_STEP_IN_PROPERTY_NAME = "stack_update_needs_step_in"; 28 29 // Applies the change to the script. 30 // The change is in form of list of chunks encoded in a single array as 31 // a series of triplets (pos1_start, pos1_end, pos2_end) 32 function ApplyPatchMultiChunk(script, diff_array, new_source, preview_only, 33 change_log) { 34 35 var old_source = script.source; 36 37 // Gather compile information about old version of script. 38 var old_compile_info = GatherCompileInfo(old_source, script); 39 40 // Build tree structures for old and new versions of the script. 41 var root_old_node = BuildCodeInfoTree(old_compile_info); 42 43 var pos_translator = new PosTranslator(diff_array); 44 45 // Analyze changes. 46 MarkChangedFunctions(root_old_node, pos_translator.GetChunks()); 47 48 // Find all SharedFunctionInfo's that were compiled from this script. 49 FindLiveSharedInfos(root_old_node, script); 50 51 // Gather compile information about new version of script. 52 var new_compile_info; 53 try { 54 new_compile_info = GatherCompileInfo(new_source, script); 55 } catch (e) { 56 var failure = 57 new Failure("Failed to compile new version of script: " + e); 58 if (e instanceof SyntaxError) { 59 var details = { 60 type: "liveedit_compile_error", 61 syntaxErrorMessage: e.message 62 }; 63 CopyErrorPositionToDetails(e, details); 64 failure.details = details; 65 } 66 throw failure; 67 } 68 var root_new_node = BuildCodeInfoTree(new_compile_info); 69 70 // Link recompiled script data with other data. 71 FindCorrespondingFunctions(root_old_node, root_new_node); 72 73 // Prepare to-do lists. 74 var replace_code_list = new Array(); 75 var link_to_old_script_list = new Array(); 76 var link_to_original_script_list = new Array(); 77 var update_positions_list = new Array(); 78 79 function HarvestTodo(old_node) { 80 function CollectDamaged(node) { 81 link_to_old_script_list.push(node); 82 for (var i = 0; i < node.children.length; i++) { 83 CollectDamaged(node.children[i]); 84 } 85 } 86 87 // Recursively collects all newly compiled functions that are going into 88 // business and should have link to the actual script updated. 89 function CollectNew(node_list) { 90 for (var i = 0; i < node_list.length; i++) { 91 link_to_original_script_list.push(node_list[i]); 92 CollectNew(node_list[i].children); 93 } 94 } 95 96 if (old_node.status == FunctionStatus.DAMAGED) { 97 CollectDamaged(old_node); 98 return; 99 } 100 if (old_node.status == FunctionStatus.UNCHANGED) { 101 update_positions_list.push(old_node); 102 } else if (old_node.status == FunctionStatus.SOURCE_CHANGED) { 103 update_positions_list.push(old_node); 104 } else if (old_node.status == FunctionStatus.CHANGED) { 105 replace_code_list.push(old_node); 106 CollectNew(old_node.unmatched_new_nodes); 107 } 108 for (var i = 0; i < old_node.children.length; i++) { 109 HarvestTodo(old_node.children[i]); 110 } 111 } 112 113 var preview_description = { 114 change_tree: DescribeChangeTree(root_old_node), 115 textual_diff: { 116 old_len: old_source.length, 117 new_len: new_source.length, 118 chunks: diff_array 119 }, 120 updated: false 121 }; 122 123 if (preview_only) { 124 return preview_description; 125 } 126 127 HarvestTodo(root_old_node); 128 129 // Collect shared infos for functions whose code need to be patched. 130 var replaced_function_infos = new Array(); 131 for (var i = 0; i < replace_code_list.length; i++) { 132 var live_shared_function_infos = 133 replace_code_list[i].live_shared_function_infos; 134 135 if (live_shared_function_infos) { 136 for (var j = 0; j < live_shared_function_infos.length; j++) { 137 replaced_function_infos.push(live_shared_function_infos[j]); 138 } 139 } 140 } 141 142 // We haven't changed anything before this line yet. 143 // Committing all changes. 144 145 // Check that function being patched is not currently on stack or drop them. 146 var dropped_functions_number = 147 CheckStackActivations(replaced_function_infos, change_log); 148 149 preview_description.stack_modified = dropped_functions_number != 0; 150 151 // Our current implementation requires client to manually issue "step in" 152 // command for correct stack state. 153 preview_description[NEEDS_STEP_IN_PROPERTY_NAME] = 154 preview_description.stack_modified; 155 156 // Start with breakpoints. Convert their line/column positions and 157 // temporary remove. 158 var break_points_restorer = TemporaryRemoveBreakPoints(script, change_log); 159 160 var old_script; 161 162 // Create an old script only if there are function that should be linked 163 // to old version. 164 if (link_to_old_script_list.length == 0) { 165 %LiveEditReplaceScript(script, new_source, null); 166 old_script = UNDEFINED; 167 } else { 168 var old_script_name = CreateNameForOldScript(script); 169 170 // Update the script text and create a new script representing an old 171 // version of the script. 172 old_script = %LiveEditReplaceScript(script, new_source, 173 old_script_name); 174 175 var link_to_old_script_report = new Array(); 176 change_log.push( { linked_to_old_script: link_to_old_script_report } ); 177 178 // We need to link to old script all former nested functions. 179 for (var i = 0; i < link_to_old_script_list.length; i++) { 180 LinkToOldScript(link_to_old_script_list[i], old_script, 181 link_to_old_script_report); 182 } 183 184 preview_description.created_script_name = old_script_name; 185 } 186 187 // Link to an actual script all the functions that we are going to use. 188 for (var i = 0; i < link_to_original_script_list.length; i++) { 189 %LiveEditFunctionSetScript( 190 link_to_original_script_list[i].info.shared_function_info, script); 191 } 192 193 for (var i = 0; i < replace_code_list.length; i++) { 194 PatchFunctionCode(replace_code_list[i], change_log); 195 } 196 197 var position_patch_report = new Array(); 198 change_log.push( {position_patched: position_patch_report} ); 199 200 for (var i = 0; i < update_positions_list.length; i++) { 201 // TODO(LiveEdit): take into account whether it's source_changed or 202 // unchanged and whether positions changed at all. 203 PatchPositions(update_positions_list[i], diff_array, 204 position_patch_report); 205 206 if (update_positions_list[i].live_shared_function_infos) { 207 update_positions_list[i].live_shared_function_infos. 208 forEach(function (info) { 209 %LiveEditFunctionSourceUpdated(info.raw_array); 210 }); 211 } 212 } 213 214 break_points_restorer(pos_translator, old_script); 215 216 preview_description.updated = true; 217 return preview_description; 218 } 219 // Function is public. 220 this.ApplyPatchMultiChunk = ApplyPatchMultiChunk; 221 222 223 // Fully compiles source string as a script. Returns Array of 224 // FunctionCompileInfo -- a descriptions of all functions of the script. 225 // Elements of array are ordered by start positions of functions (from top 226 // to bottom) in the source. Fields outer_index and next_sibling_index help 227 // to navigate the nesting structure of functions. 228 // 229 // All functions get compiled linked to script provided as parameter script. 230 // TODO(LiveEdit): consider not using actual scripts as script, because 231 // we have to manually erase all links right after compile. 232 function GatherCompileInfo(source, script) { 233 // Get function info, elements are partially sorted (it is a tree of 234 // nested functions serialized as parent followed by serialized children. 235 var raw_compile_info = %LiveEditGatherCompileInfo(script, source); 236 237 // Sort function infos by start position field. 238 var compile_info = new Array(); 239 var old_index_map = new Array(); 240 for (var i = 0; i < raw_compile_info.length; i++) { 241 var info = new FunctionCompileInfo(raw_compile_info[i]); 242 // Remove all links to the actual script. Breakpoints system and 243 // LiveEdit itself believe that any function in heap that points to a 244 // particular script is a regular function. 245 // For some functions we will restore this link later. 246 %LiveEditFunctionSetScript(info.shared_function_info, UNDEFINED); 247 compile_info.push(info); 248 old_index_map.push(i); 249 } 250 251 for (var i = 0; i < compile_info.length; i++) { 252 var k = i; 253 for (var j = i + 1; j < compile_info.length; j++) { 254 if (compile_info[k].start_position > compile_info[j].start_position) { 255 k = j; 256 } 257 } 258 if (k != i) { 259 var temp_info = compile_info[k]; 260 var temp_index = old_index_map[k]; 261 compile_info[k] = compile_info[i]; 262 old_index_map[k] = old_index_map[i]; 263 compile_info[i] = temp_info; 264 old_index_map[i] = temp_index; 265 } 266 } 267 268 // After sorting update outer_index field using old_index_map. Also 269 // set next_sibling_index field. 270 var current_index = 0; 271 272 // The recursive function, that goes over all children of a particular 273 // node (i.e. function info). 274 function ResetIndexes(new_parent_index, old_parent_index) { 275 var previous_sibling = -1; 276 while (current_index < compile_info.length && 277 compile_info[current_index].outer_index == old_parent_index) { 278 var saved_index = current_index; 279 compile_info[saved_index].outer_index = new_parent_index; 280 if (previous_sibling != -1) { 281 compile_info[previous_sibling].next_sibling_index = saved_index; 282 } 283 previous_sibling = saved_index; 284 current_index++; 285 ResetIndexes(saved_index, old_index_map[saved_index]); 286 } 287 if (previous_sibling != -1) { 288 compile_info[previous_sibling].next_sibling_index = -1; 289 } 290 } 291 292 ResetIndexes(-1, -1); 293 Assert(current_index == compile_info.length); 294 295 return compile_info; 296 } 297 298 299 // Replaces function's Code. 300 function PatchFunctionCode(old_node, change_log) { 301 var new_info = old_node.corresponding_node.info; 302 if (old_node.live_shared_function_infos) { 303 old_node.live_shared_function_infos.forEach(function (old_info) { 304 %LiveEditReplaceFunctionCode(new_info.raw_array, 305 old_info.raw_array); 306 307 // The function got a new code. However, this new code brings all new 308 // instances of SharedFunctionInfo for nested functions. However, 309 // we want the original instances to be used wherever possible. 310 // (This is because old instances and new instances will be both 311 // linked to a script and breakpoints subsystem does not really 312 // expects this; neither does LiveEdit subsystem on next call). 313 for (var i = 0; i < old_node.children.length; i++) { 314 if (old_node.children[i].corresponding_node) { 315 var corresponding_child_info = 316 old_node.children[i].corresponding_node.info. 317 shared_function_info; 318 319 if (old_node.children[i].live_shared_function_infos) { 320 old_node.children[i].live_shared_function_infos. 321 forEach(function (old_child_info) { 322 %LiveEditReplaceRefToNestedFunction( 323 old_info.info, 324 corresponding_child_info, 325 old_child_info.info); 326 }); 327 } 328 } 329 } 330 }); 331 332 change_log.push( {function_patched: new_info.function_name} ); 333 } else { 334 change_log.push( {function_patched: new_info.function_name, 335 function_info_not_found: true} ); 336 } 337 } 338 339 340 // Makes a function associated with another instance of a script (the 341 // one representing its old version). This way the function still 342 // may access its own text. 343 function LinkToOldScript(old_info_node, old_script, report_array) { 344 if (old_info_node.live_shared_function_infos) { 345 old_info_node.live_shared_function_infos. 346 forEach(function (info) { 347 %LiveEditFunctionSetScript(info.info, old_script); 348 }); 349 350 report_array.push( { name: old_info_node.info.function_name } ); 351 } else { 352 report_array.push( 353 { name: old_info_node.info.function_name, not_found: true } ); 354 } 355 } 356 357 358 // Returns function that restores breakpoints. 359 function TemporaryRemoveBreakPoints(original_script, change_log) { 360 var script_break_points = GetScriptBreakPoints(original_script); 361 362 var break_points_update_report = []; 363 change_log.push( { break_points_update: break_points_update_report } ); 364 365 var break_point_old_positions = []; 366 for (var i = 0; i < script_break_points.length; i++) { 367 var break_point = script_break_points[i]; 368 369 break_point.clear(); 370 371 // TODO(LiveEdit): be careful with resource offset here. 372 var break_point_position = Debug.findScriptSourcePosition(original_script, 373 break_point.line(), break_point.column()); 374 375 var old_position_description = { 376 position: break_point_position, 377 line: break_point.line(), 378 column: break_point.column() 379 }; 380 break_point_old_positions.push(old_position_description); 381 } 382 383 384 // Restores breakpoints and creates their copies in the "old" copy of 385 // the script. 386 return function (pos_translator, old_script_copy_opt) { 387 // Update breakpoints (change positions and restore them in old version 388 // of script. 389 for (var i = 0; i < script_break_points.length; i++) { 390 var break_point = script_break_points[i]; 391 if (old_script_copy_opt) { 392 var clone = break_point.cloneForOtherScript(old_script_copy_opt); 393 clone.set(old_script_copy_opt); 394 395 break_points_update_report.push( { 396 type: "copied_to_old", 397 id: break_point.number(), 398 new_id: clone.number(), 399 positions: break_point_old_positions[i] 400 } ); 401 } 402 403 var updated_position = pos_translator.Translate( 404 break_point_old_positions[i].position, 405 PosTranslator.ShiftWithTopInsideChunkHandler); 406 407 var new_location = 408 original_script.locationFromPosition(updated_position, false); 409 410 break_point.update_positions(new_location.line, new_location.column); 411 412 var new_position_description = { 413 position: updated_position, 414 line: new_location.line, 415 column: new_location.column 416 }; 417 418 break_point.set(original_script); 419 420 break_points_update_report.push( { type: "position_changed", 421 id: break_point.number(), 422 old_positions: break_point_old_positions[i], 423 new_positions: new_position_description 424 } ); 425 } 426 }; 427 } 428 429 430 function Assert(condition, message) { 431 if (!condition) { 432 if (message) { 433 throw "Assert " + message; 434 } else { 435 throw "Assert"; 436 } 437 } 438 } 439 440 function DiffChunk(pos1, pos2, len1, len2) { 441 this.pos1 = pos1; 442 this.pos2 = pos2; 443 this.len1 = len1; 444 this.len2 = len2; 445 } 446 447 function PosTranslator(diff_array) { 448 var chunks = new Array(); 449 var current_diff = 0; 450 for (var i = 0; i < diff_array.length; i += 3) { 451 var pos1_begin = diff_array[i]; 452 var pos2_begin = pos1_begin + current_diff; 453 var pos1_end = diff_array[i + 1]; 454 var pos2_end = diff_array[i + 2]; 455 chunks.push(new DiffChunk(pos1_begin, pos2_begin, pos1_end - pos1_begin, 456 pos2_end - pos2_begin)); 457 current_diff = pos2_end - pos1_end; 458 } 459 this.chunks = chunks; 460 } 461 PosTranslator.prototype.GetChunks = function() { 462 return this.chunks; 463 }; 464 465 PosTranslator.prototype.Translate = function(pos, inside_chunk_handler) { 466 var array = this.chunks; 467 if (array.length == 0 || pos < array[0].pos1) { 468 return pos; 469 } 470 var chunk_index1 = 0; 471 var chunk_index2 = array.length - 1; 472 473 while (chunk_index1 < chunk_index2) { 474 var middle_index = Math.floor((chunk_index1 + chunk_index2) / 2); 475 if (pos < array[middle_index + 1].pos1) { 476 chunk_index2 = middle_index; 477 } else { 478 chunk_index1 = middle_index + 1; 479 } 480 } 481 var chunk = array[chunk_index1]; 482 if (pos >= chunk.pos1 + chunk.len1) { 483 return pos + chunk.pos2 + chunk.len2 - chunk.pos1 - chunk.len1; 484 } 485 486 if (!inside_chunk_handler) { 487 inside_chunk_handler = PosTranslator.DefaultInsideChunkHandler; 488 } 489 return inside_chunk_handler(pos, chunk); 490 }; 491 492 PosTranslator.DefaultInsideChunkHandler = function(pos, diff_chunk) { 493 Assert(false, "Cannot translate position in changed area"); 494 }; 495 496 PosTranslator.ShiftWithTopInsideChunkHandler = 497 function(pos, diff_chunk) { 498 // We carelessly do not check whether we stay inside the chunk after 499 // translation. 500 return pos - diff_chunk.pos1 + diff_chunk.pos2; 501 }; 502 503 var FunctionStatus = { 504 // No change to function or its inner functions; however its positions 505 // in script may have been shifted. 506 UNCHANGED: "unchanged", 507 // The code of a function remains unchanged, but something happened inside 508 // some inner functions. 509 SOURCE_CHANGED: "source changed", 510 // The code of a function is changed or some nested function cannot be 511 // properly patched so this function must be recompiled. 512 CHANGED: "changed", 513 // Function is changed but cannot be patched. 514 DAMAGED: "damaged" 515 }; 516 517 function CodeInfoTreeNode(code_info, children, array_index) { 518 this.info = code_info; 519 this.children = children; 520 // an index in array of compile_info 521 this.array_index = array_index; 522 this.parent = UNDEFINED; 523 524 this.status = FunctionStatus.UNCHANGED; 525 // Status explanation is used for debugging purposes and will be shown 526 // in user UI if some explanations are needed. 527 this.status_explanation = UNDEFINED; 528 this.new_start_pos = UNDEFINED; 529 this.new_end_pos = UNDEFINED; 530 this.corresponding_node = UNDEFINED; 531 this.unmatched_new_nodes = UNDEFINED; 532 533 // 'Textual' correspondence/matching is weaker than 'pure' 534 // correspondence/matching. We need 'textual' level for visual presentation 535 // in UI, we use 'pure' level for actual code manipulation. 536 // Sometimes only function body is changed (functions in old and new script 537 // textually correspond), but we cannot patch the code, so we see them 538 // as an old function deleted and new function created. 539 this.textual_corresponding_node = UNDEFINED; 540 this.textually_unmatched_new_nodes = UNDEFINED; 541 542 this.live_shared_function_infos = UNDEFINED; 543 } 544 545 // From array of function infos that is implicitly a tree creates 546 // an actual tree of functions in script. 547 function BuildCodeInfoTree(code_info_array) { 548 // Throughtout all function we iterate over input array. 549 var index = 0; 550 551 // Recursive function that builds a branch of tree. 552 function BuildNode() { 553 var my_index = index; 554 index++; 555 var child_array = new Array(); 556 while (index < code_info_array.length && 557 code_info_array[index].outer_index == my_index) { 558 child_array.push(BuildNode()); 559 } 560 var node = new CodeInfoTreeNode(code_info_array[my_index], child_array, 561 my_index); 562 for (var i = 0; i < child_array.length; i++) { 563 child_array[i].parent = node; 564 } 565 return node; 566 } 567 568 var root = BuildNode(); 569 Assert(index == code_info_array.length); 570 return root; 571 } 572 573 // Applies a list of the textual diff chunks onto the tree of functions. 574 // Determines status of each function (from unchanged to damaged). However 575 // children of unchanged functions are ignored. 576 function MarkChangedFunctions(code_info_tree, chunks) { 577 578 // A convenient iterator over diff chunks that also translates 579 // positions from old to new in a current non-changed part of script. 580 var chunk_it = new function() { 581 var chunk_index = 0; 582 var pos_diff = 0; 583 this.current = function() { return chunks[chunk_index]; }; 584 this.next = function() { 585 var chunk = chunks[chunk_index]; 586 pos_diff = chunk.pos2 + chunk.len2 - (chunk.pos1 + chunk.len1); 587 chunk_index++; 588 }; 589 this.done = function() { return chunk_index >= chunks.length; }; 590 this.TranslatePos = function(pos) { return pos + pos_diff; }; 591 }; 592 593 // A recursive function that processes internals of a function and all its 594 // inner functions. Iterator chunk_it initially points to a chunk that is 595 // below function start. 596 function ProcessInternals(info_node) { 597 info_node.new_start_pos = chunk_it.TranslatePos( 598 info_node.info.start_position); 599 var child_index = 0; 600 var code_changed = false; 601 var source_changed = false; 602 // Simultaneously iterates over child functions and over chunks. 603 while (!chunk_it.done() && 604 chunk_it.current().pos1 < info_node.info.end_position) { 605 if (child_index < info_node.children.length) { 606 var child = info_node.children[child_index]; 607 608 if (child.info.end_position <= chunk_it.current().pos1) { 609 ProcessUnchangedChild(child); 610 child_index++; 611 continue; 612 } else if (child.info.start_position >= 613 chunk_it.current().pos1 + chunk_it.current().len1) { 614 code_changed = true; 615 chunk_it.next(); 616 continue; 617 } else if (child.info.start_position <= chunk_it.current().pos1 && 618 child.info.end_position >= chunk_it.current().pos1 + 619 chunk_it.current().len1) { 620 ProcessInternals(child); 621 source_changed = source_changed || 622 ( child.status != FunctionStatus.UNCHANGED ); 623 code_changed = code_changed || 624 ( child.status == FunctionStatus.DAMAGED ); 625 child_index++; 626 continue; 627 } else { 628 code_changed = true; 629 child.status = FunctionStatus.DAMAGED; 630 child.status_explanation = 631 "Text diff overlaps with function boundary"; 632 child_index++; 633 continue; 634 } 635 } else { 636 if (chunk_it.current().pos1 + chunk_it.current().len1 <= 637 info_node.info.end_position) { 638 info_node.status = FunctionStatus.CHANGED; 639 chunk_it.next(); 640 continue; 641 } else { 642 info_node.status = FunctionStatus.DAMAGED; 643 info_node.status_explanation = 644 "Text diff overlaps with function boundary"; 645 return; 646 } 647 } 648 Assert("Unreachable", false); 649 } 650 while (child_index < info_node.children.length) { 651 var child = info_node.children[child_index]; 652 ProcessUnchangedChild(child); 653 child_index++; 654 } 655 if (code_changed) { 656 info_node.status = FunctionStatus.CHANGED; 657 } else if (source_changed) { 658 info_node.status = FunctionStatus.SOURCE_CHANGED; 659 } 660 info_node.new_end_pos = 661 chunk_it.TranslatePos(info_node.info.end_position); 662 } 663 664 function ProcessUnchangedChild(node) { 665 node.new_start_pos = chunk_it.TranslatePos(node.info.start_position); 666 node.new_end_pos = chunk_it.TranslatePos(node.info.end_position); 667 } 668 669 ProcessInternals(code_info_tree); 670 } 671 672 // For each old function (if it is not damaged) tries to find a corresponding 673 // function in new script. Typically it should succeed (non-damaged functions 674 // by definition may only have changes inside their bodies). However there are 675 // reasons for correspondence not to be found; function with unmodified text 676 // in new script may become enclosed into other function; the innocent change 677 // inside function body may in fact be something like "} function B() {" that 678 // splits a function into 2 functions. 679 function FindCorrespondingFunctions(old_code_tree, new_code_tree) { 680 681 // A recursive function that tries to find a correspondence for all 682 // child functions and for their inner functions. 683 function ProcessNode(old_node, new_node) { 684 var scope_change_description = 685 IsFunctionContextLocalsChanged(old_node.info, new_node.info); 686 if (scope_change_description) { 687 old_node.status = FunctionStatus.CHANGED; 688 } 689 690 var old_children = old_node.children; 691 var new_children = new_node.children; 692 693 var unmatched_new_nodes_list = []; 694 var textually_unmatched_new_nodes_list = []; 695 696 var old_index = 0; 697 var new_index = 0; 698 while (old_index < old_children.length) { 699 if (old_children[old_index].status == FunctionStatus.DAMAGED) { 700 old_index++; 701 } else if (new_index < new_children.length) { 702 if (new_children[new_index].info.start_position < 703 old_children[old_index].new_start_pos) { 704 unmatched_new_nodes_list.push(new_children[new_index]); 705 textually_unmatched_new_nodes_list.push(new_children[new_index]); 706 new_index++; 707 } else if (new_children[new_index].info.start_position == 708 old_children[old_index].new_start_pos) { 709 if (new_children[new_index].info.end_position == 710 old_children[old_index].new_end_pos) { 711 old_children[old_index].corresponding_node = 712 new_children[new_index]; 713 old_children[old_index].textual_corresponding_node = 714 new_children[new_index]; 715 if (scope_change_description) { 716 old_children[old_index].status = FunctionStatus.DAMAGED; 717 old_children[old_index].status_explanation = 718 "Enclosing function is now incompatible. " + 719 scope_change_description; 720 old_children[old_index].corresponding_node = UNDEFINED; 721 } else if (old_children[old_index].status != 722 FunctionStatus.UNCHANGED) { 723 ProcessNode(old_children[old_index], 724 new_children[new_index]); 725 if (old_children[old_index].status == FunctionStatus.DAMAGED) { 726 unmatched_new_nodes_list.push( 727 old_children[old_index].corresponding_node); 728 old_children[old_index].corresponding_node = UNDEFINED; 729 old_node.status = FunctionStatus.CHANGED; 730 } 731 } 732 } else { 733 old_children[old_index].status = FunctionStatus.DAMAGED; 734 old_children[old_index].status_explanation = 735 "No corresponding function in new script found"; 736 old_node.status = FunctionStatus.CHANGED; 737 unmatched_new_nodes_list.push(new_children[new_index]); 738 textually_unmatched_new_nodes_list.push(new_children[new_index]); 739 } 740 new_index++; 741 old_index++; 742 } else { 743 old_children[old_index].status = FunctionStatus.DAMAGED; 744 old_children[old_index].status_explanation = 745 "No corresponding function in new script found"; 746 old_node.status = FunctionStatus.CHANGED; 747 old_index++; 748 } 749 } else { 750 old_children[old_index].status = FunctionStatus.DAMAGED; 751 old_children[old_index].status_explanation = 752 "No corresponding function in new script found"; 753 old_node.status = FunctionStatus.CHANGED; 754 old_index++; 755 } 756 } 757 758 while (new_index < new_children.length) { 759 unmatched_new_nodes_list.push(new_children[new_index]); 760 textually_unmatched_new_nodes_list.push(new_children[new_index]); 761 new_index++; 762 } 763 764 if (old_node.status == FunctionStatus.CHANGED) { 765 if (old_node.info.param_num != new_node.info.param_num) { 766 old_node.status = FunctionStatus.DAMAGED; 767 old_node.status_explanation = "Changed parameter number: " + 768 old_node.info.param_num + " and " + new_node.info.param_num; 769 } 770 } 771 old_node.unmatched_new_nodes = unmatched_new_nodes_list; 772 old_node.textually_unmatched_new_nodes = 773 textually_unmatched_new_nodes_list; 774 } 775 776 ProcessNode(old_code_tree, new_code_tree); 777 778 old_code_tree.corresponding_node = new_code_tree; 779 old_code_tree.textual_corresponding_node = new_code_tree; 780 781 Assert(old_code_tree.status != FunctionStatus.DAMAGED, 782 "Script became damaged"); 783 } 784 785 function FindLiveSharedInfos(old_code_tree, script) { 786 var shared_raw_list = %LiveEditFindSharedFunctionInfosForScript(script); 787 788 var shared_infos = new Array(); 789 790 for (var i = 0; i < shared_raw_list.length; i++) { 791 shared_infos.push(new SharedInfoWrapper(shared_raw_list[i])); 792 } 793 794 // Finds all SharedFunctionInfos that corresponds to compile info 795 // in old version of the script. 796 function FindFunctionInfos(compile_info) { 797 var wrappers = []; 798 799 for (var i = 0; i < shared_infos.length; i++) { 800 var wrapper = shared_infos[i]; 801 if (wrapper.start_position == compile_info.start_position && 802 wrapper.end_position == compile_info.end_position) { 803 wrappers.push(wrapper); 804 } 805 } 806 807 if (wrappers.length > 0) { 808 return wrappers; 809 } 810 } 811 812 function TraverseTree(node) { 813 node.live_shared_function_infos = FindFunctionInfos(node.info); 814 815 for (var i = 0; i < node.children.length; i++) { 816 TraverseTree(node.children[i]); 817 } 818 } 819 820 TraverseTree(old_code_tree); 821 } 822 823 824 // An object describing function compilation details. Its index fields 825 // apply to indexes inside array that stores these objects. 826 function FunctionCompileInfo(raw_array) { 827 this.function_name = raw_array[0]; 828 this.start_position = raw_array[1]; 829 this.end_position = raw_array[2]; 830 this.param_num = raw_array[3]; 831 this.code = raw_array[4]; 832 this.code_scope_info = raw_array[5]; 833 this.scope_info = raw_array[6]; 834 this.outer_index = raw_array[7]; 835 this.shared_function_info = raw_array[8]; 836 this.next_sibling_index = null; 837 this.raw_array = raw_array; 838 } 839 840 function SharedInfoWrapper(raw_array) { 841 this.function_name = raw_array[0]; 842 this.start_position = raw_array[1]; 843 this.end_position = raw_array[2]; 844 this.info = raw_array[3]; 845 this.raw_array = raw_array; 846 } 847 848 // Changes positions (including all statements) in function. 849 function PatchPositions(old_info_node, diff_array, report_array) { 850 if (old_info_node.live_shared_function_infos) { 851 old_info_node.live_shared_function_infos.forEach(function (info) { 852 %LiveEditPatchFunctionPositions(info.raw_array, 853 diff_array); 854 }); 855 856 report_array.push( { name: old_info_node.info.function_name } ); 857 } else { 858 // TODO(LiveEdit): function is not compiled yet or is already collected. 859 report_array.push( 860 { name: old_info_node.info.function_name, info_not_found: true } ); 861 } 862 } 863 864 // Adds a suffix to script name to mark that it is old version. 865 function CreateNameForOldScript(script) { 866 // TODO(635): try better than this; support several changes. 867 return script.name + " (old)"; 868 } 869 870 // Compares a function scope heap structure, old and new version, whether it 871 // changed or not. Returns explanation if they differ. 872 function IsFunctionContextLocalsChanged(function_info1, function_info2) { 873 var scope_info1 = function_info1.scope_info; 874 var scope_info2 = function_info2.scope_info; 875 876 var scope_info1_text; 877 var scope_info2_text; 878 879 if (scope_info1) { 880 scope_info1_text = scope_info1.toString(); 881 } else { 882 scope_info1_text = ""; 883 } 884 if (scope_info2) { 885 scope_info2_text = scope_info2.toString(); 886 } else { 887 scope_info2_text = ""; 888 } 889 890 if (scope_info1_text != scope_info2_text) { 891 return "Variable map changed: [" + scope_info1_text + 892 "] => [" + scope_info2_text + "]"; 893 } 894 // No differences. Return undefined. 895 return; 896 } 897 898 // Minifier forward declaration. 899 var FunctionPatchabilityStatus; 900 901 // For array of wrapped shared function infos checks that none of them 902 // have activations on stack (of any thread). Throws a Failure exception 903 // if this proves to be false. 904 function CheckStackActivations(shared_wrapper_list, change_log) { 905 var shared_list = new Array(); 906 for (var i = 0; i < shared_wrapper_list.length; i++) { 907 shared_list[i] = shared_wrapper_list[i].info; 908 } 909 var result = %LiveEditCheckAndDropActivations(shared_list, true); 910 if (result[shared_list.length]) { 911 // Extra array element may contain error message. 912 throw new Failure(result[shared_list.length]); 913 } 914 915 var problems = new Array(); 916 var dropped = new Array(); 917 for (var i = 0; i < shared_list.length; i++) { 918 var shared = shared_wrapper_list[i]; 919 if (result[i] == FunctionPatchabilityStatus.REPLACED_ON_ACTIVE_STACK) { 920 dropped.push({ name: shared.function_name } ); 921 } else if (result[i] != FunctionPatchabilityStatus.AVAILABLE_FOR_PATCH) { 922 var description = { 923 name: shared.function_name, 924 start_pos: shared.start_position, 925 end_pos: shared.end_position, 926 replace_problem: 927 FunctionPatchabilityStatus.SymbolName(result[i]) 928 }; 929 problems.push(description); 930 } 931 } 932 if (dropped.length > 0) { 933 change_log.push({ dropped_from_stack: dropped }); 934 } 935 if (problems.length > 0) { 936 change_log.push( { functions_on_stack: problems } ); 937 throw new Failure("Blocked by functions on stack"); 938 } 939 940 return dropped.length; 941 } 942 943 // A copy of the FunctionPatchabilityStatus enum from liveedit.h 944 var FunctionPatchabilityStatus = { 945 AVAILABLE_FOR_PATCH: 1, 946 BLOCKED_ON_ACTIVE_STACK: 2, 947 BLOCKED_ON_OTHER_STACK: 3, 948 BLOCKED_UNDER_NATIVE_CODE: 4, 949 REPLACED_ON_ACTIVE_STACK: 5, 950 BLOCKED_UNDER_GENERATOR: 6, 951 BLOCKED_ACTIVE_GENERATOR: 7 952 }; 953 954 FunctionPatchabilityStatus.SymbolName = function(code) { 955 var enumeration = FunctionPatchabilityStatus; 956 for (name in enumeration) { 957 if (enumeration[name] == code) { 958 return name; 959 } 960 } 961 }; 962 963 964 // A logical failure in liveedit process. This means that change_log 965 // is valid and consistent description of what happened. 966 function Failure(message) { 967 this.message = message; 968 } 969 // Function (constructor) is public. 970 this.Failure = Failure; 971 972 Failure.prototype.toString = function() { 973 return "LiveEdit Failure: " + this.message; 974 }; 975 976 function CopyErrorPositionToDetails(e, details) { 977 function createPositionStruct(script, position) { 978 if (position == -1) return; 979 var location = script.locationFromPosition(position, true); 980 if (location == null) return; 981 return { 982 line: location.line + 1, 983 column: location.column + 1, 984 position: position 985 }; 986 } 987 988 if (!("scriptObject" in e) || !("startPosition" in e)) { 989 return; 990 } 991 992 var script = e.scriptObject; 993 994 var position_struct = { 995 start: createPositionStruct(script, e.startPosition), 996 end: createPositionStruct(script, e.endPosition) 997 }; 998 details.position = position_struct; 999 } 1000 1001 // A testing entry. 1002 function GetPcFromSourcePos(func, source_pos) { 1003 return %GetFunctionCodePositionFromSource(func, source_pos); 1004 } 1005 // Function is public. 1006 this.GetPcFromSourcePos = GetPcFromSourcePos; 1007 1008 // LiveEdit main entry point: changes a script text to a new string. 1009 function SetScriptSource(script, new_source, preview_only, change_log) { 1010 var old_source = script.source; 1011 var diff = CompareStrings(old_source, new_source); 1012 return ApplyPatchMultiChunk(script, diff, new_source, preview_only, 1013 change_log); 1014 } 1015 // Function is public. 1016 this.SetScriptSource = SetScriptSource; 1017 1018 function CompareStrings(s1, s2) { 1019 return %LiveEditCompareStrings(s1, s2); 1020 } 1021 1022 // Applies the change to the script. 1023 // The change is always a substring (change_pos, change_pos + change_len) 1024 // being replaced with a completely different string new_str. 1025 // This API is a legacy and is obsolete. 1026 // 1027 // @param {Script} script that is being changed 1028 // @param {Array} change_log a list that collects engineer-readable 1029 // description of what happened. 1030 function ApplySingleChunkPatch(script, change_pos, change_len, new_str, 1031 change_log) { 1032 var old_source = script.source; 1033 1034 // Prepare new source string. 1035 var new_source = old_source.substring(0, change_pos) + 1036 new_str + old_source.substring(change_pos + change_len); 1037 1038 return ApplyPatchMultiChunk(script, 1039 [ change_pos, change_pos + change_len, change_pos + new_str.length], 1040 new_source, false, change_log); 1041 } 1042 1043 // Creates JSON description for a change tree. 1044 function DescribeChangeTree(old_code_tree) { 1045 1046 function ProcessOldNode(node) { 1047 var child_infos = []; 1048 for (var i = 0; i < node.children.length; i++) { 1049 var child = node.children[i]; 1050 if (child.status != FunctionStatus.UNCHANGED) { 1051 child_infos.push(ProcessOldNode(child)); 1052 } 1053 } 1054 var new_child_infos = []; 1055 if (node.textually_unmatched_new_nodes) { 1056 for (var i = 0; i < node.textually_unmatched_new_nodes.length; i++) { 1057 var child = node.textually_unmatched_new_nodes[i]; 1058 new_child_infos.push(ProcessNewNode(child)); 1059 } 1060 } 1061 var res = { 1062 name: node.info.function_name, 1063 positions: DescribePositions(node), 1064 status: node.status, 1065 children: child_infos, 1066 new_children: new_child_infos 1067 }; 1068 if (node.status_explanation) { 1069 res.status_explanation = node.status_explanation; 1070 } 1071 if (node.textual_corresponding_node) { 1072 res.new_positions = DescribePositions(node.textual_corresponding_node); 1073 } 1074 return res; 1075 } 1076 1077 function ProcessNewNode(node) { 1078 var child_infos = []; 1079 // Do not list ancestors. 1080 if (false) { 1081 for (var i = 0; i < node.children.length; i++) { 1082 child_infos.push(ProcessNewNode(node.children[i])); 1083 } 1084 } 1085 var res = { 1086 name: node.info.function_name, 1087 positions: DescribePositions(node), 1088 children: child_infos, 1089 }; 1090 return res; 1091 } 1092 1093 function DescribePositions(node) { 1094 return { 1095 start_position: node.info.start_position, 1096 end_position: node.info.end_position 1097 }; 1098 } 1099 1100 return ProcessOldNode(old_code_tree); 1101 } 1102 1103 // Restarts call frame and returns value similar to what LiveEdit returns. 1104 function RestartFrame(frame_mirror) { 1105 var result = frame_mirror.restart(); 1106 if (IS_STRING(result)) { 1107 throw new Failure("Failed to restart frame: " + result); 1108 } 1109 var result = {}; 1110 result[NEEDS_STEP_IN_PROPERTY_NAME] = true; 1111 return result; 1112 } 1113 // Function is public. 1114 this.RestartFrame = RestartFrame; 1115 1116 // Functions are public for tests. 1117 this.TestApi = { 1118 PosTranslator: PosTranslator, 1119 CompareStrings: CompareStrings, 1120 ApplySingleChunkPatch: ApplySingleChunkPatch 1121 }; 1122}; 1123