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