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