// Copyright 2012 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // LiveEdit feature implementation. The script should be executed after // debug-debugger.js. // A LiveEdit namespace. It contains functions that modifies JavaScript code // according to changes of script source (if possible). // // When new script source is put in, the difference is calculated textually, // in form of list of delete/add/change chunks. The functions that include // change chunk(s) get recompiled, or their enclosing functions are // recompiled instead. // If the function may not be recompiled (e.g. it was completely erased in new // version of the script) it remains unchanged, but the code that could // create a new instance of this function goes away. An old version of script // is created to back up this obsolete function. // All unchanged functions have their positions updated accordingly. // // LiveEdit namespace is declared inside a single function constructor. Debug.LiveEdit = new function() { // Forward declaration for minifier. var FunctionStatus; var NEEDS_STEP_IN_PROPERTY_NAME = "stack_update_needs_step_in"; // Applies the change to the script. // The change is in form of list of chunks encoded in a single array as // a series of triplets (pos1_start, pos1_end, pos2_end) function ApplyPatchMultiChunk(script, diff_array, new_source, preview_only, change_log) { var old_source = script.source; // Gather compile information about old version of script. var old_compile_info = GatherCompileInfo(old_source, script); // Build tree structures for old and new versions of the script. var root_old_node = BuildCodeInfoTree(old_compile_info); var pos_translator = new PosTranslator(diff_array); // Analyze changes. MarkChangedFunctions(root_old_node, pos_translator.GetChunks()); // Find all SharedFunctionInfo's that were compiled from this script. FindLiveSharedInfos(root_old_node, script); // Gather compile information about new version of script. var new_compile_info; try { new_compile_info = GatherCompileInfo(new_source, script); } catch (e) { var failure = new Failure("Failed to compile new version of script: " + e); if (e instanceof SyntaxError) { var details = { type: "liveedit_compile_error", syntaxErrorMessage: e.message }; CopyErrorPositionToDetails(e, details); failure.details = details; } throw failure; } var root_new_node = BuildCodeInfoTree(new_compile_info); // Link recompiled script data with other data. FindCorrespondingFunctions(root_old_node, root_new_node); // Prepare to-do lists. var replace_code_list = new Array(); var link_to_old_script_list = new Array(); var link_to_original_script_list = new Array(); var update_positions_list = new Array(); function HarvestTodo(old_node) { function CollectDamaged(node) { link_to_old_script_list.push(node); for (var i = 0; i < node.children.length; i++) { CollectDamaged(node.children[i]); } } // Recursively collects all newly compiled functions that are going into // business and should have link to the actual script updated. function CollectNew(node_list) { for (var i = 0; i < node_list.length; i++) { link_to_original_script_list.push(node_list[i]); CollectNew(node_list[i].children); } } if (old_node.status == FunctionStatus.DAMAGED) { CollectDamaged(old_node); return; } if (old_node.status == FunctionStatus.UNCHANGED) { update_positions_list.push(old_node); } else if (old_node.status == FunctionStatus.SOURCE_CHANGED) { update_positions_list.push(old_node); } else if (old_node.status == FunctionStatus.CHANGED) { replace_code_list.push(old_node); CollectNew(old_node.unmatched_new_nodes); } for (var i = 0; i < old_node.children.length; i++) { HarvestTodo(old_node.children[i]); } } var preview_description = { change_tree: DescribeChangeTree(root_old_node), textual_diff: { old_len: old_source.length, new_len: new_source.length, chunks: diff_array }, updated: false }; if (preview_only) { return preview_description; } HarvestTodo(root_old_node); // Collect shared infos for functions whose code need to be patched. var replaced_function_infos = new Array(); for (var i = 0; i < replace_code_list.length; i++) { var live_shared_function_infos = replace_code_list[i].live_shared_function_infos; if (live_shared_function_infos) { for (var j = 0; j < live_shared_function_infos.length; j++) { replaced_function_infos.push(live_shared_function_infos[j]); } } } // We haven't changed anything before this line yet. // Committing all changes. // Check that function being patched is not currently on stack or drop them. var dropped_functions_number = CheckStackActivations(replaced_function_infos, change_log); preview_description.stack_modified = dropped_functions_number != 0; // Our current implementation requires client to manually issue "step in" // command for correct stack state. preview_description[NEEDS_STEP_IN_PROPERTY_NAME] = preview_description.stack_modified; // Start with breakpoints. Convert their line/column positions and // temporary remove. var break_points_restorer = TemporaryRemoveBreakPoints(script, change_log); var old_script; // Create an old script only if there are function that should be linked // to old version. if (link_to_old_script_list.length == 0) { %LiveEditReplaceScript(script, new_source, null); old_script = UNDEFINED; } else { var old_script_name = CreateNameForOldScript(script); // Update the script text and create a new script representing an old // version of the script. old_script = %LiveEditReplaceScript(script, new_source, old_script_name); var link_to_old_script_report = new Array(); change_log.push( { linked_to_old_script: link_to_old_script_report } ); // We need to link to old script all former nested functions. for (var i = 0; i < link_to_old_script_list.length; i++) { LinkToOldScript(link_to_old_script_list[i], old_script, link_to_old_script_report); } preview_description.created_script_name = old_script_name; } // Link to an actual script all the functions that we are going to use. for (var i = 0; i < link_to_original_script_list.length; i++) { %LiveEditFunctionSetScript( link_to_original_script_list[i].info.shared_function_info, script); } for (var i = 0; i < replace_code_list.length; i++) { PatchFunctionCode(replace_code_list[i], change_log); } var position_patch_report = new Array(); change_log.push( {position_patched: position_patch_report} ); for (var i = 0; i < update_positions_list.length; i++) { // TODO(LiveEdit): take into account whether it's source_changed or // unchanged and whether positions changed at all. PatchPositions(update_positions_list[i], diff_array, position_patch_report); if (update_positions_list[i].live_shared_function_infos) { update_positions_list[i].live_shared_function_infos. forEach(function (info) { %LiveEditFunctionSourceUpdated(info.raw_array); }); } } break_points_restorer(pos_translator, old_script); preview_description.updated = true; return preview_description; } // Function is public. this.ApplyPatchMultiChunk = ApplyPatchMultiChunk; // 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 // to bottom) in the source. Fields outer_index and next_sibling_index help // to navigate the nesting structure of functions. // // All functions get compiled linked to script provided as parameter script. // TODO(LiveEdit): consider not using actual scripts as script, because // we have to manually erase all links right after compile. function GatherCompileInfo(source, script) { // 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. var compile_info = new Array(); var old_index_map = new Array(); for (var i = 0; i < raw_compile_info.length; i++) { var info = new FunctionCompileInfo(raw_compile_info[i]); // Remove all links to the actual script. Breakpoints system and // LiveEdit itself believe that any function in heap that points to a // particular script is a regular function. // For some functions we will restore this link later. %LiveEditFunctionSetScript(info.shared_function_info, UNDEFINED); compile_info.push(info); old_index_map.push(i); } for (var i = 0; i < compile_info.length; i++) { var k = i; for (var j = i + 1; j < compile_info.length; j++) { if (compile_info[k].start_position > compile_info[j].start_position) { k = j; } } if (k != i) { var temp_info = compile_info[k]; var temp_index = old_index_map[k]; compile_info[k] = compile_info[i]; old_index_map[k] = old_index_map[i]; compile_info[i] = temp_info; old_index_map[i] = temp_index; } } // After sorting update outer_index field using old_index_map. Also // set next_sibling_index field. var current_index = 0; // The recursive function, that goes over all children of a particular // node (i.e. function info). function ResetIndexes(new_parent_index, old_parent_index) { var previous_sibling = -1; while (current_index < compile_info.length && compile_info[current_index].outer_index == old_parent_index) { var saved_index = current_index; compile_info[saved_index].outer_index = new_parent_index; if (previous_sibling != -1) { compile_info[previous_sibling].next_sibling_index = saved_index; } previous_sibling = saved_index; current_index++; ResetIndexes(saved_index, old_index_map[saved_index]); } if (previous_sibling != -1) { compile_info[previous_sibling].next_sibling_index = -1; } } ResetIndexes(-1, -1); Assert(current_index == compile_info.length); return compile_info; } // Replaces function's Code. function PatchFunctionCode(old_node, change_log) { var new_info = old_node.corresponding_node.info; if (old_node.live_shared_function_infos) { old_node.live_shared_function_infos.forEach(function (old_info) { %LiveEditReplaceFunctionCode(new_info.raw_array, old_info.raw_array); // The function got a new code. However, this new code brings all new // instances of SharedFunctionInfo for nested functions. However, // we want the original instances to be used wherever possible. // (This is because old instances and new instances will be both // linked to a script and breakpoints subsystem does not really // expects this; neither does LiveEdit subsystem on next call). for (var i = 0; i < old_node.children.length; i++) { if (old_node.children[i].corresponding_node) { var corresponding_child_info = old_node.children[i].corresponding_node.info. shared_function_info; if (old_node.children[i].live_shared_function_infos) { old_node.children[i].live_shared_function_infos. forEach(function (old_child_info) { %LiveEditReplaceRefToNestedFunction( old_info.info, corresponding_child_info, old_child_info.info); }); } } } }); change_log.push( {function_patched: new_info.function_name} ); } else { change_log.push( {function_patched: new_info.function_name, function_info_not_found: true} ); } } // Makes a function associated with another instance of a script (the // one representing its old version). This way the function still // may access its own text. function LinkToOldScript(old_info_node, old_script, report_array) { if (old_info_node.live_shared_function_infos) { old_info_node.live_shared_function_infos. forEach(function (info) { %LiveEditFunctionSetScript(info.info, old_script); }); report_array.push( { name: old_info_node.info.function_name } ); } else { report_array.push( { name: old_info_node.info.function_name, not_found: true } ); } } // Returns function that restores breakpoints. function TemporaryRemoveBreakPoints(original_script, change_log) { var script_break_points = GetScriptBreakPoints(original_script); var break_points_update_report = []; change_log.push( { break_points_update: break_points_update_report } ); var break_point_old_positions = []; for (var i = 0; i < script_break_points.length; i++) { var break_point = script_break_points[i]; break_point.clear(); // TODO(LiveEdit): be careful with resource offset here. var break_point_position = Debug.findScriptSourcePosition(original_script, break_point.line(), break_point.column()); var old_position_description = { position: break_point_position, line: break_point.line(), column: break_point.column() }; break_point_old_positions.push(old_position_description); } // Restores breakpoints and creates their copies in the "old" copy of // the script. return function (pos_translator, old_script_copy_opt) { // Update breakpoints (change positions and restore them in old version // of script. for (var i = 0; i < script_break_points.length; i++) { var break_point = script_break_points[i]; if (old_script_copy_opt) { var clone = break_point.cloneForOtherScript(old_script_copy_opt); clone.set(old_script_copy_opt); break_points_update_report.push( { type: "copied_to_old", id: break_point.number(), new_id: clone.number(), positions: break_point_old_positions[i] } ); } var updated_position = pos_translator.Translate( break_point_old_positions[i].position, PosTranslator.ShiftWithTopInsideChunkHandler); var new_location = original_script.locationFromPosition(updated_position, false); break_point.update_positions(new_location.line, new_location.column); var new_position_description = { position: updated_position, line: new_location.line, column: new_location.column }; break_point.set(original_script); break_points_update_report.push( { type: "position_changed", id: break_point.number(), old_positions: break_point_old_positions[i], new_positions: new_position_description } ); } }; } function Assert(condition, message) { if (!condition) { if (message) { throw "Assert " + message; } else { throw "Assert"; } } } function DiffChunk(pos1, pos2, len1, len2) { this.pos1 = pos1; this.pos2 = pos2; this.len1 = len1; this.len2 = len2; } function PosTranslator(diff_array) { var chunks = new Array(); var current_diff = 0; for (var i = 0; i < diff_array.length; i += 3) { var pos1_begin = diff_array[i]; var pos2_begin = pos1_begin + current_diff; var pos1_end = diff_array[i + 1]; var pos2_end = diff_array[i + 2]; chunks.push(new DiffChunk(pos1_begin, pos2_begin, pos1_end - pos1_begin, pos2_end - pos2_begin)); current_diff = pos2_end - pos1_end; } this.chunks = chunks; } PosTranslator.prototype.GetChunks = function() { return this.chunks; }; PosTranslator.prototype.Translate = function(pos, inside_chunk_handler) { var array = this.chunks; if (array.length == 0 || pos < array[0].pos1) { return pos; } var chunk_index1 = 0; var chunk_index2 = array.length - 1; while (chunk_index1 < chunk_index2) { var middle_index = Math.floor((chunk_index1 + chunk_index2) / 2); if (pos < array[middle_index + 1].pos1) { chunk_index2 = middle_index; } else { chunk_index1 = middle_index + 1; } } var chunk = array[chunk_index1]; if (pos >= chunk.pos1 + chunk.len1) { return pos + chunk.pos2 + chunk.len2 - chunk.pos1 - chunk.len1; } if (!inside_chunk_handler) { inside_chunk_handler = PosTranslator.DefaultInsideChunkHandler; } return inside_chunk_handler(pos, chunk); }; PosTranslator.DefaultInsideChunkHandler = function(pos, diff_chunk) { Assert(false, "Cannot translate position in changed area"); }; PosTranslator.ShiftWithTopInsideChunkHandler = function(pos, diff_chunk) { // We carelessly do not check whether we stay inside the chunk after // translation. return pos - diff_chunk.pos1 + diff_chunk.pos2; }; var FunctionStatus = { // No change to function or its inner functions; however its positions // in script may have been shifted. UNCHANGED: "unchanged", // The code of a function remains unchanged, but something happened inside // some inner functions. SOURCE_CHANGED: "source changed", // The code of a function is changed or some nested function cannot be // properly patched so this function must be recompiled. CHANGED: "changed", // Function is changed but cannot be patched. DAMAGED: "damaged" }; function CodeInfoTreeNode(code_info, children, array_index) { this.info = code_info; this.children = children; // an index in array of compile_info this.array_index = array_index; this.parent = UNDEFINED; this.status = FunctionStatus.UNCHANGED; // Status explanation is used for debugging purposes and will be shown // in user UI if some explanations are needed. this.status_explanation = UNDEFINED; this.new_start_pos = UNDEFINED; this.new_end_pos = UNDEFINED; this.corresponding_node = UNDEFINED; this.unmatched_new_nodes = UNDEFINED; // 'Textual' correspondence/matching is weaker than 'pure' // correspondence/matching. We need 'textual' level for visual presentation // in UI, we use 'pure' level for actual code manipulation. // Sometimes only function body is changed (functions in old and new script // textually correspond), but we cannot patch the code, so we see them // as an old function deleted and new function created. this.textual_corresponding_node = UNDEFINED; this.textually_unmatched_new_nodes = UNDEFINED; this.live_shared_function_infos = UNDEFINED; } // From array of function infos that is implicitly a tree creates // an actual tree of functions in script. function BuildCodeInfoTree(code_info_array) { // Throughtout all function we iterate over input array. var index = 0; // Recursive function that builds a branch of tree. function BuildNode() { var my_index = index; index++; var child_array = new Array(); while (index < code_info_array.length && code_info_array[index].outer_index == my_index) { child_array.push(BuildNode()); } var node = new CodeInfoTreeNode(code_info_array[my_index], child_array, my_index); for (var i = 0; i < child_array.length; i++) { child_array[i].parent = node; } return node; } var root = BuildNode(); Assert(index == code_info_array.length); return root; } // Applies a list of the textual diff chunks onto the tree of functions. // Determines status of each function (from unchanged to damaged). However // children of unchanged functions are ignored. function MarkChangedFunctions(code_info_tree, chunks) { // A convenient iterator over diff chunks that also translates // positions from old to new in a current non-changed part of script. var chunk_it = new function() { var chunk_index = 0; var pos_diff = 0; this.current = function() { return chunks[chunk_index]; }; this.next = function() { var chunk = chunks[chunk_index]; pos_diff = chunk.pos2 + chunk.len2 - (chunk.pos1 + chunk.len1); chunk_index++; }; this.done = function() { return chunk_index >= chunks.length; }; this.TranslatePos = function(pos) { return pos + pos_diff; }; }; // A recursive function that processes internals of a function and all its // inner functions. Iterator chunk_it initially points to a chunk that is // below function start. function ProcessInternals(info_node) { info_node.new_start_pos = chunk_it.TranslatePos( info_node.info.start_position); var child_index = 0; var code_changed = false; var source_changed = false; // Simultaneously iterates over child functions and over chunks. while (!chunk_it.done() && chunk_it.current().pos1 < info_node.info.end_position) { if (child_index < info_node.children.length) { var child = info_node.children[child_index]; if (child.info.end_position <= chunk_it.current().pos1) { ProcessUnchangedChild(child); child_index++; continue; } else if (child.info.start_position >= chunk_it.current().pos1 + chunk_it.current().len1) { code_changed = true; chunk_it.next(); continue; } else if (child.info.start_position <= chunk_it.current().pos1 && child.info.end_position >= chunk_it.current().pos1 + chunk_it.current().len1) { ProcessInternals(child); source_changed = source_changed || ( child.status != FunctionStatus.UNCHANGED ); code_changed = code_changed || ( child.status == FunctionStatus.DAMAGED ); child_index++; continue; } else { code_changed = true; child.status = FunctionStatus.DAMAGED; child.status_explanation = "Text diff overlaps with function boundary"; child_index++; continue; } } else { if (chunk_it.current().pos1 + chunk_it.current().len1 <= info_node.info.end_position) { info_node.status = FunctionStatus.CHANGED; chunk_it.next(); continue; } else { info_node.status = FunctionStatus.DAMAGED; info_node.status_explanation = "Text diff overlaps with function boundary"; return; } } Assert("Unreachable", false); } while (child_index < info_node.children.length) { var child = info_node.children[child_index]; ProcessUnchangedChild(child); child_index++; } if (code_changed) { info_node.status = FunctionStatus.CHANGED; } else if (source_changed) { info_node.status = FunctionStatus.SOURCE_CHANGED; } info_node.new_end_pos = chunk_it.TranslatePos(info_node.info.end_position); } function ProcessUnchangedChild(node) { node.new_start_pos = chunk_it.TranslatePos(node.info.start_position); node.new_end_pos = chunk_it.TranslatePos(node.info.end_position); } ProcessInternals(code_info_tree); } // For each old function (if it is not damaged) tries to find a corresponding // function in new script. Typically it should succeed (non-damaged functions // by definition may only have changes inside their bodies). However there are // reasons for correspondence not to be found; function with unmodified text // in new script may become enclosed into other function; the innocent change // inside function body may in fact be something like "} function B() {" that // splits a function into 2 functions. function FindCorrespondingFunctions(old_code_tree, new_code_tree) { // A recursive function that tries to find a correspondence for all // child functions and for their inner functions. function ProcessNode(old_node, new_node) { var scope_change_description = IsFunctionContextLocalsChanged(old_node.info, new_node.info); if (scope_change_description) { old_node.status = FunctionStatus.CHANGED; } var old_children = old_node.children; var new_children = new_node.children; var unmatched_new_nodes_list = []; var textually_unmatched_new_nodes_list = []; var old_index = 0; var new_index = 0; while (old_index < old_children.length) { if (old_children[old_index].status == FunctionStatus.DAMAGED) { old_index++; } else if (new_index < new_children.length) { if (new_children[new_index].info.start_position < old_children[old_index].new_start_pos) { unmatched_new_nodes_list.push(new_children[new_index]); textually_unmatched_new_nodes_list.push(new_children[new_index]); new_index++; } else if (new_children[new_index].info.start_position == old_children[old_index].new_start_pos) { if (new_children[new_index].info.end_position == old_children[old_index].new_end_pos) { old_children[old_index].corresponding_node = new_children[new_index]; old_children[old_index].textual_corresponding_node = new_children[new_index]; if (scope_change_description) { old_children[old_index].status = FunctionStatus.DAMAGED; old_children[old_index].status_explanation = "Enclosing function is now incompatible. " + scope_change_description; old_children[old_index].corresponding_node = UNDEFINED; } else if (old_children[old_index].status != FunctionStatus.UNCHANGED) { ProcessNode(old_children[old_index], new_children[new_index]); if (old_children[old_index].status == FunctionStatus.DAMAGED) { unmatched_new_nodes_list.push( old_children[old_index].corresponding_node); old_children[old_index].corresponding_node = UNDEFINED; old_node.status = FunctionStatus.CHANGED; } } } else { old_children[old_index].status = FunctionStatus.DAMAGED; old_children[old_index].status_explanation = "No corresponding function in new script found"; old_node.status = FunctionStatus.CHANGED; unmatched_new_nodes_list.push(new_children[new_index]); textually_unmatched_new_nodes_list.push(new_children[new_index]); } new_index++; old_index++; } else { old_children[old_index].status = FunctionStatus.DAMAGED; old_children[old_index].status_explanation = "No corresponding function in new script found"; old_node.status = FunctionStatus.CHANGED; old_index++; } } else { old_children[old_index].status = FunctionStatus.DAMAGED; old_children[old_index].status_explanation = "No corresponding function in new script found"; old_node.status = FunctionStatus.CHANGED; old_index++; } } while (new_index < new_children.length) { unmatched_new_nodes_list.push(new_children[new_index]); textually_unmatched_new_nodes_list.push(new_children[new_index]); new_index++; } if (old_node.status == FunctionStatus.CHANGED) { if (old_node.info.param_num != new_node.info.param_num) { old_node.status = FunctionStatus.DAMAGED; old_node.status_explanation = "Changed parameter number: " + old_node.info.param_num + " and " + new_node.info.param_num; } } old_node.unmatched_new_nodes = unmatched_new_nodes_list; old_node.textually_unmatched_new_nodes = textually_unmatched_new_nodes_list; } ProcessNode(old_code_tree, new_code_tree); old_code_tree.corresponding_node = new_code_tree; old_code_tree.textual_corresponding_node = new_code_tree; Assert(old_code_tree.status != FunctionStatus.DAMAGED, "Script became damaged"); } function FindLiveSharedInfos(old_code_tree, script) { var shared_raw_list = %LiveEditFindSharedFunctionInfosForScript(script); var shared_infos = new Array(); for (var i = 0; i < shared_raw_list.length; i++) { shared_infos.push(new SharedInfoWrapper(shared_raw_list[i])); } // Finds all SharedFunctionInfos that corresponds to compile info // in old version of the script. function FindFunctionInfos(compile_info) { var wrappers = []; for (var i = 0; i < shared_infos.length; i++) { var wrapper = shared_infos[i]; if (wrapper.start_position == compile_info.start_position && wrapper.end_position == compile_info.end_position) { wrappers.push(wrapper); } } if (wrappers.length > 0) { return wrappers; } } function TraverseTree(node) { node.live_shared_function_infos = FindFunctionInfos(node.info); for (var i = 0; i < node.children.length; i++) { TraverseTree(node.children[i]); } } TraverseTree(old_code_tree); } // An object describing function compilation details. Its index fields // apply to indexes inside array that stores these objects. function FunctionCompileInfo(raw_array) { this.function_name = raw_array[0]; this.start_position = raw_array[1]; this.end_position = raw_array[2]; this.param_num = raw_array[3]; this.code = raw_array[4]; this.code_scope_info = raw_array[5]; this.scope_info = raw_array[6]; this.outer_index = raw_array[7]; this.shared_function_info = raw_array[8]; this.next_sibling_index = null; this.raw_array = raw_array; } function SharedInfoWrapper(raw_array) { this.function_name = raw_array[0]; this.start_position = raw_array[1]; this.end_position = raw_array[2]; this.info = raw_array[3]; this.raw_array = raw_array; } // Changes positions (including all statements) in function. function PatchPositions(old_info_node, diff_array, report_array) { if (old_info_node.live_shared_function_infos) { old_info_node.live_shared_function_infos.forEach(function (info) { %LiveEditPatchFunctionPositions(info.raw_array, diff_array); }); report_array.push( { name: old_info_node.info.function_name } ); } else { // TODO(LiveEdit): function is not compiled yet or is already collected. report_array.push( { name: old_info_node.info.function_name, info_not_found: true } ); } } // Adds a suffix to script name to mark that it is old version. function CreateNameForOldScript(script) { // TODO(635): try better than this; support several changes. return script.name + " (old)"; } // Compares a function scope heap structure, old and new version, whether it // changed or not. Returns explanation if they differ. function IsFunctionContextLocalsChanged(function_info1, function_info2) { var scope_info1 = function_info1.scope_info; var scope_info2 = function_info2.scope_info; var scope_info1_text; var scope_info2_text; if (scope_info1) { scope_info1_text = scope_info1.toString(); } else { scope_info1_text = ""; } if (scope_info2) { scope_info2_text = scope_info2.toString(); } else { scope_info2_text = ""; } if (scope_info1_text != scope_info2_text) { return "Variable map changed: [" + scope_info1_text + "] => [" + scope_info2_text + "]"; } // No differences. Return undefined. return; } // Minifier forward declaration. var FunctionPatchabilityStatus; // For array of wrapped shared function infos checks that none of them // have activations on stack (of any thread). Throws a Failure exception // if this proves to be false. function CheckStackActivations(shared_wrapper_list, change_log) { var shared_list = new Array(); for (var i = 0; i < shared_wrapper_list.length; i++) { shared_list[i] = shared_wrapper_list[i].info; } var result = %LiveEditCheckAndDropActivations(shared_list, true); if (result[shared_list.length]) { // Extra array element may contain error message. throw new Failure(result[shared_list.length]); } var problems = new Array(); var dropped = new Array(); for (var i = 0; i < shared_list.length; i++) { var shared = shared_wrapper_list[i]; if (result[i] == FunctionPatchabilityStatus.REPLACED_ON_ACTIVE_STACK) { dropped.push({ name: shared.function_name } ); } else if (result[i] != FunctionPatchabilityStatus.AVAILABLE_FOR_PATCH) { var description = { name: shared.function_name, start_pos: shared.start_position, end_pos: shared.end_position, replace_problem: FunctionPatchabilityStatus.SymbolName(result[i]) }; problems.push(description); } } if (dropped.length > 0) { change_log.push({ dropped_from_stack: dropped }); } if (problems.length > 0) { change_log.push( { functions_on_stack: problems } ); throw new Failure("Blocked by functions on stack"); } return dropped.length; } // A copy of the FunctionPatchabilityStatus enum from liveedit.h var FunctionPatchabilityStatus = { AVAILABLE_FOR_PATCH: 1, BLOCKED_ON_ACTIVE_STACK: 2, BLOCKED_ON_OTHER_STACK: 3, BLOCKED_UNDER_NATIVE_CODE: 4, REPLACED_ON_ACTIVE_STACK: 5, BLOCKED_UNDER_GENERATOR: 6, BLOCKED_ACTIVE_GENERATOR: 7 }; FunctionPatchabilityStatus.SymbolName = function(code) { var enumeration = FunctionPatchabilityStatus; for (name in enumeration) { if (enumeration[name] == code) { return name; } } }; // A logical failure in liveedit process. This means that change_log // is valid and consistent description of what happened. function Failure(message) { this.message = message; } // Function (constructor) is public. this.Failure = Failure; Failure.prototype.toString = function() { return "LiveEdit Failure: " + this.message; }; function CopyErrorPositionToDetails(e, details) { function createPositionStruct(script, position) { if (position == -1) return; var location = script.locationFromPosition(position, true); if (location == null) return; return { line: location.line + 1, column: location.column + 1, position: position }; } if (!("scriptObject" in e) || !("startPosition" in e)) { return; } var script = e.scriptObject; var position_struct = { start: createPositionStruct(script, e.startPosition), end: createPositionStruct(script, e.endPosition) }; details.position = position_struct; } // A testing entry. function GetPcFromSourcePos(func, source_pos) { return %GetFunctionCodePositionFromSource(func, source_pos); } // Function is public. this.GetPcFromSourcePos = GetPcFromSourcePos; // LiveEdit main entry point: changes a script text to a new string. function SetScriptSource(script, new_source, preview_only, change_log) { var old_source = script.source; var diff = CompareStrings(old_source, new_source); return ApplyPatchMultiChunk(script, diff, new_source, preview_only, change_log); } // Function is public. this.SetScriptSource = SetScriptSource; function CompareStrings(s1, s2) { return %LiveEditCompareStrings(s1, s2); } // Applies the change to the script. // The change is always a substring (change_pos, change_pos + change_len) // being replaced with a completely different string new_str. // This API is a legacy and is obsolete. // // @param {Script} script that is being changed // @param {Array} change_log a list that collects engineer-readable // description of what happened. function ApplySingleChunkPatch(script, change_pos, change_len, new_str, change_log) { var old_source = script.source; // Prepare new source string. var new_source = old_source.substring(0, change_pos) + new_str + old_source.substring(change_pos + change_len); return ApplyPatchMultiChunk(script, [ change_pos, change_pos + change_len, change_pos + new_str.length], new_source, false, change_log); } // Creates JSON description for a change tree. function DescribeChangeTree(old_code_tree) { function ProcessOldNode(node) { var child_infos = []; for (var i = 0; i < node.children.length; i++) { var child = node.children[i]; if (child.status != FunctionStatus.UNCHANGED) { child_infos.push(ProcessOldNode(child)); } } var new_child_infos = []; if (node.textually_unmatched_new_nodes) { for (var i = 0; i < node.textually_unmatched_new_nodes.length; i++) { var child = node.textually_unmatched_new_nodes[i]; new_child_infos.push(ProcessNewNode(child)); } } var res = { name: node.info.function_name, positions: DescribePositions(node), status: node.status, children: child_infos, new_children: new_child_infos }; if (node.status_explanation) { res.status_explanation = node.status_explanation; } if (node.textual_corresponding_node) { res.new_positions = DescribePositions(node.textual_corresponding_node); } return res; } function ProcessNewNode(node) { var child_infos = []; // Do not list ancestors. if (false) { for (var i = 0; i < node.children.length; i++) { child_infos.push(ProcessNewNode(node.children[i])); } } var res = { name: node.info.function_name, positions: DescribePositions(node), children: child_infos, }; return res; } function DescribePositions(node) { return { start_position: node.info.start_position, end_position: node.info.end_position }; } return ProcessOldNode(old_code_tree); } // Restarts call frame and returns value similar to what LiveEdit returns. function RestartFrame(frame_mirror) { var result = frame_mirror.restart(); if (IS_STRING(result)) { throw new Failure("Failed to restart frame: " + result); } var result = {}; result[NEEDS_STEP_IN_PROPERTY_NAME] = true; return result; } // Function is public. this.RestartFrame = RestartFrame; // Functions are public for tests. this.TestApi = { PosTranslator: PosTranslator, CompareStrings: CompareStrings, ApplySingleChunkPatch: ApplySingleChunkPatch }; };