nitdbg: Added command to print the content of any SequenceRead collection
[nit.git] / src / debugger.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Lucas Bajolet <lucas.bajolet@gmail.com>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Debugging of a nit program using the NaiveInterpreter
18 module debugger
19
20 import breakpoint
21 intrude import naive_interpreter
22
23 redef class ToolContext
24 # -d
25 var opt_debugger_mode: OptionBool = new OptionBool("Launches the target program with the debugger attached to it", "-d")
26
27 redef init
28 do
29 super
30 self.option_context.add_option(self.opt_debugger_mode)
31 end
32 end
33
34 redef class ModelBuilder
35 # Execute the program from the entry point (Sys::main) of the `mainmodule'
36 # `arguments' are the command-line arguments in order
37 # REQUIRE that:
38 # 1. the AST is fully loaded.
39 # 2. the model is fully built.
40 # 3. the instructions are fully analysed.
41 fun run_debugger(mainmodule: MModule, arguments: Array[String])
42 do
43 var time0 = get_time
44 self.toolcontext.info("*** START INTERPRETING ***", 1)
45
46 var interpreter = new Debugger(self, mainmodule, arguments)
47
48 init_naive_interpreter(interpreter, mainmodule)
49
50 var time1 = get_time
51 self.toolcontext.info("*** END INTERPRETING: {time1-time0} ***", 2)
52 end
53 end
54
55 # The class extending NaiveInterpreter by adding debugging methods
56 class Debugger
57 super NaiveInterpreter
58
59 # Keeps the frame count in memory to find when to stop
60 # and launch the command prompt after a step out call
61 var step_stack_count = 1
62
63 # Triggers a step over an instruction in a nit program
64 var stop_after_step_over_trigger = true
65
66 # Triggers a step out of an instruction
67 var stop_after_step_out_trigger= false
68
69 # Triggers a step in a instruction (enters a function
70 # if the instruction is a function call)
71 var step_in_trigger = false
72
73 # HashMap containing the breakpoints bound to a file
74 var breakpoints = new HashMap[String, HashSet[Breakpoint]]
75
76 # Contains the current file
77 var curr_file = ""
78
79 # Aliases hashmap (maps an alias to a variable name)
80 var aliases = new HashMap[String, String]
81
82 #######################################################################
83 ## Execution of statement function ##
84 #######################################################################
85
86 # Main loop, every call to a debug command is done here
87 redef fun stmt(n: nullable AExpr)
88 do
89 if n == null then return
90
91 var frame = self.frame
92 var old = frame.current_node
93 frame.current_node = n
94
95 steps_fun_call(n)
96
97 breakpoint_check(n)
98
99 n.stmt(self)
100 frame.current_node = old
101 end
102
103 # Encpasulates the behaviour for step over/out
104 private fun steps_fun_call(n: AExpr)
105 do
106 if self.stop_after_step_over_trigger then
107 if self.frames.length <= self.step_stack_count then
108 n.debug("Execute stmt {n.to_s}")
109 while process_debug_command(gets) do end
110 end
111 else if self.stop_after_step_out_trigger then
112 if frames.length < self.step_stack_count then
113 n.debug("Execute stmt {n.to_s}")
114 while process_debug_command(gets) do end
115 end
116 else if step_in_trigger then
117 n.debug("Execute stmt {n.to_s}")
118 while process_debug_command(gets) do end
119 end
120 end
121
122 # Checks if a breakpoint is encountered, and launches the debugging prompt if true
123 private fun breakpoint_check(n: AExpr)
124 do
125 var currFileNameSplit = self.frame.current_node.location.file.filename.to_s.split_with("/")
126
127 self.curr_file = currFileNameSplit[currFileNameSplit.length-1]
128
129 var breakpoint = find_breakpoint(curr_file, n.location.line_start)
130
131 if breakpoints.keys.has(curr_file) and breakpoint != null then
132
133 breakpoint.check_in
134
135 if not breakpoint.is_valid
136 then
137 remove_breakpoint(curr_file, n.location.line_start)
138 end
139
140 n.debug("Execute stmt {n.to_s}")
141 while process_debug_command(gets) do end
142 end
143 end
144
145 #######################################################################
146 ## Processing commands functions ##
147 #######################################################################
148
149 # Takes a user command as a parameter
150 #
151 # Returns a boolean value, representing whether or not to
152 # continue reading commands from the console input
153 fun process_debug_command(command:String): Bool
154 do
155 # For lisibility
156 print "\n"
157
158 # Kills the current program
159 if command == "kill" then
160 abort
161 # Step-out command
162 else if command == "finish"
163 then
164 return step_out
165 # Step-in command
166 else if command == "s"
167 then
168 return step_in
169 # Step-over command
170 else if command == "n" then
171 return step_over
172 # Continues execution until the end
173 else if command == "c" then
174 return continue_exec
175 else
176 var parts_of_command = command.split_with(' ')
177 # Shows the value of a variable in the current frame
178 if parts_of_command[0] == "p" or parts_of_command[0] == "print" then
179 print_command(parts_of_command)
180 # Places a breakpoint on line x of file y
181 else if parts_of_command[0] == "break" or parts_of_command[0] == "b"
182 then
183 process_place_break_fun(parts_of_command)
184 # Places a temporary breakpoint on line x of file y
185 else if parts_of_command[0] == "tbreak" and (parts_of_command.length == 2 or parts_of_command.length == 3)
186 then
187 process_place_tbreak_fun(parts_of_command)
188 # Removes a breakpoint on line x of file y
189 else if parts_of_command[0] == "d" or parts_of_command[0] == "delete" then
190 process_remove_break_fun(parts_of_command)
191 # Sets an alias for a variable
192 else if parts_of_command.length == 3 and parts_of_command[1] == "as"
193 then
194 add_alias(parts_of_command[0], parts_of_command[2])
195 # Lists all the commands available
196 else
197 list_commands
198 end
199 end
200 return true
201 end
202
203 #######################################################################
204 ## Processing specific command functions ##
205 #######################################################################
206
207 # Sets the flags to step-over an instruction in the current file
208 fun step_over: Bool
209 do
210 self.step_stack_count = frames.length
211 self.stop_after_step_over_trigger = true
212 self.stop_after_step_out_trigger = false
213 self.step_in_trigger = false
214 return false
215 end
216
217 #Sets the flags to step-out of a function
218 fun step_out: Bool
219 do
220 self.stop_after_step_over_trigger = false
221 self.stop_after_step_out_trigger = true
222 self.step_in_trigger = false
223 self.step_stack_count = frames.length
224 return false
225 end
226
227 # Sets the flags to step-in an instruction
228 fun step_in: Bool
229 do
230 self.step_in_trigger = true
231 self.stop_after_step_over_trigger = false
232 self.stop_after_step_out_trigger = false
233 return false
234 end
235
236 # Sets the flags to continue execution
237 fun continue_exec: Bool
238 do
239 self.stop_after_step_over_trigger = false
240 self.stop_after_step_out_trigger = false
241 self.step_in_trigger = false
242 return false
243 end
244
245 # Prints the demanded variable in the command
246 #
247 # The name of the variable in in position 1 of the array 'parts_of_command'
248 fun print_command(parts_of_command: Array[String])
249 do
250 if parts_of_command[1] == "*" then
251 var map_of_instances = frame.map
252
253 var keys = map_of_instances.iterator
254
255 print "Variables collection : \n"
256
257 for instance in map_of_instances.keys do
258 print "Variable {instance.to_s}, Instance {map_of_instances[instance].to_s}"
259 end
260
261 print "\nEnd of current instruction \n"
262 else if parts_of_command[1].has('[') and parts_of_command[1].has(']') then
263 process_array_command(parts_of_command)
264 else
265 var instance = seek_variable(get_real_variable_name(parts_of_command[1]), frame)
266
267 if instance != null
268 then
269 print_instance(instance)
270 else
271 print "Cannot find variable {parts_of_command[1]}"
272 end
273 end
274 end
275
276 # Processes the input string to know where to put a breakpoint
277 fun process_place_break_fun(parts_of_command: Array[String])
278 do
279 var bp = get_breakpoint_from_command(parts_of_command)
280 if bp != null then
281 place_breakpoint(bp)
282 else
283 list_commands
284 end
285 end
286
287 # Returns a breakpoint containing the informations stored in the command
288 fun get_breakpoint_from_command(parts_of_command: Array[String]): nullable Breakpoint
289 do
290 if parts_of_command[1].is_numeric then
291 return new Breakpoint(parts_of_command[1].to_i, curr_file)
292 else if parts_of_command.length >= 3 and parts_of_command[2].is_numeric then
293 return new Breakpoint(parts_of_command[2].to_i, parts_of_command[1])
294 else
295 return null
296 end
297 end
298
299 # Processes the command of removing a breakpoint on specified line and file
300 fun process_remove_break_fun(parts_of_command: Array[String])
301 do
302 if parts_of_command[1].is_numeric then
303 remove_breakpoint(self.curr_file, parts_of_command[1].to_i)
304 else if parts_of_command.length >= 3 and parts_of_command[2].is_numeric then
305 remove_breakpoint(parts_of_command[1], parts_of_command[2].to_i)
306 else
307 list_commands
308 end
309 end
310
311 # Processes an array print command
312 fun process_array_command(parts_of_command: Array[String])
313 do
314 var index_of_first_brace = parts_of_command[1].index_of('[')
315 var variable_name = get_real_variable_name(parts_of_command[1].substring(0,index_of_first_brace))
316 var braces = parts_of_command[1].substring_from(index_of_first_brace)
317
318 var indexes = remove_braces(braces)
319
320 var index_array = new Array[Array[Int]]
321
322 if indexes != null then
323 for index in indexes do
324 var temp_indexes_array = process_index(index)
325 if temp_indexes_array != null then
326 index_array.push(temp_indexes_array)
327 #print index_array.last
328 end
329 end
330 end
331
332 var instance = seek_variable(variable_name, frame)
333
334 if instance != null then
335 print_nested_collection(instance, index_array, 0, variable_name, "")
336 else
337 print "Cannot find variable {variable_name}"
338 end
339 end
340
341 #######################################################################
342 ## Alias management functions ##
343 #######################################################################
344
345 # Adds a new alias to the tables
346 fun add_alias(var_represented: String, alias: String)
347 do
348 self.aliases[alias] = var_represented
349 end
350
351 # Gets the real name of a variable hidden by an alias
352 fun get_variable_name_by_alias(alias: String): nullable String
353 do
354 if self.aliases.keys.has(alias) then
355 return self.aliases[alias]
356 end
357
358 return null
359 end
360
361 # Gets the variable named by name, whether it is an alias or not
362 fun get_real_variable_name(name: String): String
363 do
364 var explode_string = name.split_with(".")
365 var final_string = new Buffer
366 for i in explode_string do
367 var alias_resolved = get_variable_name_by_alias(i)
368 if alias_resolved != null then
369 final_string.append(get_real_variable_name(alias_resolved))
370 else
371 final_string.append(i)
372 end
373 final_string.append(".")
374 end
375
376 return final_string.substring(0,final_string.length-1).to_s
377 end
378
379 #######################################################################
380 ## Print functions ##
381 #######################################################################
382
383 # Prints an object instance and its attributes if it has some
384 #
385 # If it is a primitive type, its value is directly printed
386 fun print_instance(instance: Instance)
387 do
388 if instance isa MutableInstance then
389 var attributes = instance.attributes
390 print "Object : {instance}"
391
392 for current_attribute in attributes.keys do
393 print "Attribute : {current_attribute.to_s} \nValeur : {attributes[current_attribute].to_s}"
394 end
395 else
396 print "Found variable {instance}"
397 end
398 end
399
400 # Prints the attributes demanded in a SequenceRead
401 # Used recursively to print nested collections
402 fun print_nested_collection(instance: Instance, indexes: Array[Array[Int]], depth: Int, variable_name: String, depth_string: String)
403 do
404 var collection: nullable SequenceRead[Object] = null
405 var real_collection_length: nullable Int = null
406
407 if instance isa MutableInstance then
408 real_collection_length = get_collection_instance_real_length(instance)
409 collection = get_primary_collection(instance)
410 end
411
412 if collection != null and real_collection_length != null then
413 for i in indexes[depth] do
414 if i >= 0 and i < real_collection_length then
415 if depth == indexes.length-1 then
416 print "{variable_name}{depth_string}[{i}] = {collection[i]}"
417 else
418 var item_i = collection[i]
419
420 if item_i isa MutableInstance then
421 print_nested_collection(item_i, indexes, depth+1, variable_name, depth_string+"[{i}]")
422 else
423 print "The item at {variable_name}{depth_string}[{i}] is not a collection"
424 print item_i
425 end
426 end
427 else
428 print "Out of bounds exception : i = {i} and collection_length = {real_collection_length.to_s}"
429
430 if i < 0 then
431 continue
432 else if i >= real_collection_length then
433 break
434 end
435 end
436 end
437 else
438 if collection == null then
439 print "Cannot find {variable_name}{depth_string}"
440 else if real_collection_length != null then
441 print "Cannot find attribute length in {instance}"
442 else
443 print "Unknown error."
444 abort
445 end
446 end
447 end
448
449 #######################################################################
450 ## Variable Exploration functions ##
451 #######################################################################
452
453 # Seeks a variable from the current frame called 'variable_path', can introspect complex objects using function get_variable_in_mutable_instance
454 private fun seek_variable(variable_path: String, frame: Frame): nullable Instance
455 do
456 var full_variable = variable_path.split_with(".")
457
458 var full_variable_iterator = full_variable.iterator
459
460 var first_instance = get_variable_in_frame(full_variable_iterator.item, frame)
461
462 if first_instance == null then return null
463
464 if full_variable.length <= 1 then return first_instance
465
466 full_variable_iterator.next
467
468 if not (first_instance isa MutableInstance and full_variable_iterator.is_ok) then return null
469
470 return get_variable_in_mutable_instance(first_instance, full_variable_iterator)
471 end
472
473 # Gets a variable 'variable_name' contained in the frame 'frame'
474 private fun get_variable_in_frame(variable_name: String, frame: Frame): nullable Instance
475 do
476 if variable_name == "self" then
477 if frame.arguments.length >= 1 then return frame.arguments.first
478 end
479
480 var map_of_instances = frame.map
481
482 for key in map_of_instances.keys do
483 if key.to_s == variable_name then
484 return map_of_instances[key]
485 end
486 end
487
488 return null
489 end
490
491 # Gets an attribute 'attribute_name' contained in variable 'variable'
492 fun get_attribute_in_mutable_instance(variable: MutableInstance, attribute_name: String): nullable MAttribute
493 do
494 var map_of_attributes = variable.attributes
495
496 for key in map_of_attributes.keys do
497 if key.to_s.substring_from(1) == attribute_name then
498 return key
499 end
500 end
501
502 return null
503 end
504
505 # Recursive function, returns the variable described by 'total_chain'
506 fun get_variable_in_mutable_instance(variable: MutableInstance, iterator: Iterator[String]): nullable Instance
507 do
508 var attribute = get_attribute_in_mutable_instance(variable, iterator.item)
509
510 if attribute == null then return null
511
512 iterator.next
513
514 if iterator.is_ok then
515 var new_variable = variable.attributes[attribute]
516 if new_variable isa MutableInstance then
517 return get_variable_in_mutable_instance(new_variable, iterator)
518 else
519 return null
520 end
521 else
522 return variable.attributes[attribute]
523 end
524 end
525
526 #######################################################################
527 ## Array exploring functions ##
528 #######################################################################
529
530 # Gets the length of a collection
531 # Used by the debugger, else if we call Collection.length, it returns the capacity instead
532 fun get_collection_instance_real_length(collection: MutableInstance): nullable Int
533 do
534 var collection_length_attribute = get_attribute_in_mutable_instance(collection, "length")
535
536 var real_collection_length: nullable Int = null
537
538 if collection_length_attribute != null then
539 var primitive_length_instance = collection.attributes[collection_length_attribute]
540 if primitive_length_instance isa PrimitiveInstance[Int] then
541 return primitive_length_instance.val
542 end
543 end
544
545 return null
546 end
547
548 # Processes the indexes of a print array call
549 # Returns an array containing all the indexes demanded
550 fun process_index(index_string: String): nullable Array[Int]
551 do
552 var from_end_index = index_string.index_of('.')
553 var to_start_index = index_string.last_index_of('.')
554
555 if from_end_index != -1 and to_start_index != -1 then
556 var index_from_string = index_string.substring(0,from_end_index)
557 var index_to_string = index_string.substring_from(to_start_index+1)
558
559 if index_from_string.is_numeric and index_to_string.is_numeric then
560 var result_array = new Array[Int]
561
562 var index_from = index_from_string.to_i
563 var index_to = index_to_string.to_i
564
565 for i in [index_from..index_to] do
566 result_array.push(i)
567 end
568
569 return result_array
570 end
571 else
572 if index_string.is_numeric
573 then
574 var result_array = new Array[Int]
575
576 result_array.push(index_string.to_i)
577
578 return result_array
579 else
580 return null
581 end
582 end
583
584 return null
585 end
586
587 # Gets a collection in a MutableInstance
588 fun get_primary_collection(container: MutableInstance): nullable SequenceRead[Object]
589 do
590 var items_of_array = get_attribute_in_mutable_instance(container, "items")
591 if items_of_array != null then
592 var array = container.attributes[items_of_array]
593
594 if array isa PrimitiveInstance[Object] then
595 var sequenceRead_final = array.val
596 if sequenceRead_final isa SequenceRead[Object] then
597 return sequenceRead_final
598 end
599 end
600 end
601
602 return null
603 end
604
605 # Removes the braces '[' ']' in a print array command
606 # Returns an array containing their content
607 fun remove_braces(braces: String): nullable Array[String]
608 do
609 var buffer = new Buffer
610
611 var result_array = new Array[String]
612
613 var number_of_opening_brackets = 0
614 var number_of_closing_brackets = 0
615
616 var last_was_opening_bracket = false
617
618 for i in braces do
619 if i == '[' then
620 if last_was_opening_bracket then
621 return null
622 end
623
624 number_of_opening_brackets += 1
625 last_was_opening_bracket = true
626 else if i == ']' then
627 if not last_was_opening_bracket then
628 return null
629 end
630
631 result_array.push(buffer.to_s)
632 buffer.clear
633 number_of_closing_brackets += 1
634 last_was_opening_bracket = false
635 else if i.is_numeric or i == '.' then
636 buffer.append(i.to_s)
637 else if not i == ' ' then
638 return null
639 end
640 end
641
642 if number_of_opening_brackets != number_of_closing_brackets then
643 return null
644 end
645
646 return result_array
647 end
648
649 #######################################################################
650 ## Breakpoint placing functions ##
651 #######################################################################
652
653 # Places a breakpoint on line 'line_to_break' for file 'file_to_break'
654 fun place_breakpoint(breakpoint: Breakpoint)
655 do
656 if not self.breakpoints.keys.has(breakpoint.file) then
657 self.breakpoints[breakpoint.file] = new HashSet[Breakpoint]
658 end
659 if find_breakpoint(breakpoint.file, breakpoint.line) == null then
660 self.breakpoints[breakpoint.file].add(breakpoint)
661 print "Breakpoint added on line {breakpoint.line} for file {breakpoint.file}"
662 else
663 print "Breakpoint already present on line {breakpoint.line} for file {breakpoint.file}"
664 end
665 end
666
667 #Places a breakpoint that will trigger once and be destroyed afterwards
668 fun process_place_tbreak_fun(parts_of_command: Array[String])
669 do
670 var bp = get_breakpoint_from_command(parts_of_command)
671 if bp != null
672 then
673 bp.set_max_breaks(1)
674 place_breakpoint(bp)
675 else
676 list_commands
677 end
678 end
679
680 #######################################################################
681 ## Breakpoint removing functions ##
682 #######################################################################
683
684 # Removes a breakpoint on line 'line_to_break' for file 'file_to_break'
685 fun remove_breakpoint(file_to_break: String, line_to_break: Int)
686 do
687 if self.breakpoints.keys.has(file_to_break) then
688 var bp = find_breakpoint(file_to_break, line_to_break)
689
690 if bp != null then
691 self.breakpoints[file_to_break].remove(bp)
692 print "Breakpoint removed on line {line_to_break} for file {file_to_break}"
693 return
694 end
695 end
696
697 print "No breakpoint existing on line {line_to_break} for file {file_to_break}"
698 end
699
700 #######################################################################
701 ## Breakpoint searching functions ##
702 #######################################################################
703
704 # Finds a breakpoint for 'file' and 'line' in the class HashMap
705 fun find_breakpoint(file: String, line: Int): nullable Breakpoint
706 do
707 if self.breakpoints.keys.has(file)
708 then
709 for i in self.breakpoints[file]
710 do
711 if i.line == line
712 then
713 return i
714 end
715 end
716 end
717
718 return null
719 end
720
721 #######################################################################
722 ## Command listing function ##
723 #######################################################################
724
725 # Lists the commands available when using the debugger
726 fun list_commands
727 do
728 print "\nCommand not recognized\n"
729 print "Commands accepted : \n"
730 print "[break/b] line : Adds a breakpoint on line *line_nb* of the current file\n"
731 print "[break/b] file_name line_nb : Adds a breakpoint on line *line_nb* of file *file_name* \n"
732 print "[p/print] variable : [p/print] * shows the status of all the variables\n"
733 print "s : steps in on the current function\n"
734 print "n : steps-over the current instruction\n"
735 print "finish : steps out of the current function\n"
736 print "variable as alias : Adds an alias called *alias* for the variable *variable*"
737 print "An alias can reference another alias\n"
738 print "[d/delete] line_nb : Removes a breakpoint on line *line_nb* of the current file \n"
739 print "[d/delete] file_name line_nb : Removes a breakpoint on line *line_nb* of file *file_name* \n"
740 print "kill : kills the current program (Exits with an error and stack trace)\n"
741 end
742
743 end