1% Copyright (C) 2012-2017,2018 John E. Davis
2%
3% This file is part of the S-Lang Library and may be distributed under the
4% terms of the GNU General Public License.  See the file COPYING for
5% more information.
6%---------------------------------------------------------------------------
7%
8% This file implements the core a simple debugger.  It needs to be wrapped
9% by routines that implement the Debugger_Methods.
10%
11% Public functions:
12%   sldb_methods()
13%   sldb_stop ();
14%   sldb_start ();
15%   sldb_set_breakpoint ();
16%
17%
18%  Notes:
19%
20%    If a file was not compiled with bos/eos hooks, then debugging of
21%    it may be limited due to the lack of line number information.
22%
23%
24require ("print");
25
26private variable Debugger_Methods = struct
27{
28   list,          % list (file, linemin, linemax)
29   vmessage,      % vmessage (fmt, args...)
30   read_input,    % input = read_input (prompt, default)
31   pprint,        % pprint(obj) % pprint the value of an object
32   quit,          % quit (and kill) the program
33   exit		  % exit the debugger but not the program
34};
35
36private define output ()
37{
38   variable args = __pop_args (_NARGS);
39   (@Debugger_Methods.vmessage)(__push_args(args));
40}
41
42private define quit_method ()
43{
44   output ("Program exiting\n");
45   exit (0);
46}
47Debugger_Methods.quit = &quit_method;
48
49private define exit_method ()
50{
51   output ("Leaving the debugger\n");
52}
53Debugger_Methods.exit = &exit_method;
54
55define sldb_methods ()
56{
57   return Debugger_Methods;
58}
59define sldb_initialize ();	       % This should be overridden
60
61define sldb_stop();
62
63private variable Depth = 0;
64private variable Stop_Depth	= 0;
65private variable Debugger_Step	= 0;
66private variable STEP_NEXT	= 1;
67private variable STEP_STEP	= 2;
68private variable STEP_FINISH	= 3;
69private variable STEP_EXIT	= 4;
70private variable Breakpoints = NULL;
71private variable Breakpoint_Number = 1;
72private variable Current_Frame;
73private variable Max_Current_Frame;
74private variable Last_List_Line = 0;
75private variable Last_Cmd_Line = NULL;
76private variable Last_Cmd = NULL;
77private variable Prompt = "(SLdb) ";
78private variable Startup_PID = NULL;
79
80private define new_breakpoints ()
81{
82   Breakpoints = Assoc_Type[Int_Type, 0];
83   Breakpoint_Number = 1;
84}
85
86private define check_breakpoints ()
87{
88   if (Breakpoints == NULL)
89     new_breakpoints ();
90}
91
92define sldb_set_breakpoint (pos)
93{
94   variable bp;
95
96   check_breakpoints ();
97   bp = Breakpoint_Number;
98   Breakpoints[pos] = bp;
99   Breakpoint_Number++;
100
101   output ("breakpoint #%d set at %s\n", bp, pos);
102   return bp;
103}
104
105private define make_breakpoint_name (file, line)
106{
107   return sprintf ("%S:%d", file, line);
108}
109
110private define eval_in_frame (frame, expr, num_on_stack, print_fun)
111{
112   variable boseos = _boseos_info;
113   variable bofeof = _bofeof_info;
114   expr = sprintf ("_boseos_info=0; _bofeof_info=0; _use_frame_namespace(%d); %s; _bofeof_info=%d; _boseos_info=%d;",
115		   frame, expr, bofeof, boseos);
116   variable depth = _stkdepth () - num_on_stack;
117   eval (expr);
118
119   variable n = _stkdepth () - depth;
120   if (print_fun == NULL)
121     return n;
122
123   loop (n)
124     {
125	variable val = ();
126	(@print_fun) (val);
127     }
128   return n;
129}
130
131private define break_cmd (cmd, args, file, line)
132{
133   variable bp;
134   if (strlen (args) == 0)
135     bp = make_breakpoint_name (file, line);
136   else if (_slang_guess_type (args) == Int_Type)
137     bp = make_breakpoint_name (file, integer (args));
138   else
139     {
140	bp = args;
141	if (0 == is_substr (args, ":"))
142	  {
143	  }
144     }
145
146   () = sldb_set_breakpoint (bp);
147   return 0;
148}
149
150private define display_file_and_line (file, linemin, linemax)
151{
152   if (file == "***string***")
153     return;
154
155   if (linemin < 1)
156     linemin = 1;
157   if (linemax < linemin)
158     linemax = linemin;
159
160   (@Debugger_Methods.list)(file, linemin, linemax);
161}
162
163private define finish_cmd (cmd, args, file, line)
164{
165   %variable fun = _get_frame_info (Max_Current_Frame).function;
166   variable fun = _get_frame_info (Current_Frame).function;
167   if (fun == NULL) fun = "<top-level>";
168   output ("Run until exit from %s\n", fun);
169   Debugger_Step = STEP_FINISH;
170   Stop_Depth = Depth-1;
171   return 1;
172}
173
174private define next_cmd (cmd, args, file, line)
175{
176   Debugger_Step = STEP_NEXT;
177   Stop_Depth = Depth;
178   return 1;
179}
180
181private define step_cmd (cmd, args, file, line)
182{
183   Debugger_Step = STEP_STEP;
184   Stop_Depth = Depth + 1;
185   return 1;
186}
187
188private define delete_cmd (cmd, args, file, line)
189{
190   variable bp = make_breakpoint_name (file, line);
191   variable n = Breakpoints[bp];
192   if (n)
193     {
194	Breakpoints[bp] = 0;
195	output ("Deleted breakpoint #%d\n", n);
196	return 0;
197     }
198   if (args == "")
199     {
200	new_breakpoints ();
201	output ("Deleted all breakpoints\n");
202	return 0;
203     }
204
205   variable keys = assoc_get_keys (Breakpoints);
206   variable vals = assoc_get_values (Breakpoints);
207
208   foreach (eval (sprintf ("[%s]", args)))
209     {
210	bp = ();
211	variable i = wherefirst (vals == bp);
212	if (i == NULL)
213	  continue;
214	assoc_delete_key (Breakpoints, keys[i]);
215	output ("Deleted breakpoint %d\n", bp);
216     }
217   return 0;
218}
219
220private define continue_cmd (cmd, args, file, line)
221{
222   Debugger_Step = 0;
223   return 1;
224}
225
226private define watch_cmd (cmd, args, file, line)
227{
228   output ("%s is not implemented\n", cmd);
229   return 0;
230}
231
232private define exit_cmd (cmd, args, file, line)
233{
234   sldb_stop ();
235   (@Debugger_Methods.exit) ();
236   return 1;
237}
238
239private define quit_cmd (cmd, args, file, line)
240{
241   variable prompt = "Are you sure you want to quit (and kill) the program? (y/n) ";
242   variable y = (@Debugger_Methods.read_input)(prompt, NULL);
243   y = strup (y);
244   !if (strlen (y))
245     return 0;
246   if (y[0] != 'Y')
247     {
248	output ("Try using 'exit' to leave the debugger");
249	return 0;
250     }
251   sldb_stop ();
252   (@Debugger_Methods.quit)();
253   return 1;
254}
255
256private define simple_print (v)
257{
258   if (length (v) <= 1)
259     print (v, &v);
260   output ("%S\n", v);
261}
262
263private define pretty_print (v)
264{
265   variable p = Debugger_Methods.pprint;
266   if (p == NULL)
267     {
268	simple_print (v);
269	return;
270     }
271   (@p)(v);
272}
273
274private define print_expr (print_fun, expr)
275{
276   variable info = _get_frame_info (Current_Frame);
277   variable localvars = info.locals;
278
279   if (localvars == NULL)
280     {
281	() = eval_in_frame (Current_Frame, expr, 0, print_fun);
282	return;
283     }
284
285   % Create a dummy function and call it with the values of the local-vars
286   % The idea is that variables that are initialized will be arguments, and
287   % others will just be locals
288   variable a = Assoc_Type[];
289   foreach (localvars)
290     {
291	variable lvar = ();
292	try
293	  {
294	     a[lvar] = _get_frame_variable (Current_Frame, lvar);
295	  }
296	catch VariableUninitializedError;
297     }
298   variable inited_vars = assoc_get_keys (a);
299   variable uninited_vars = String_Type[0];
300   foreach (localvars)
301     {
302	lvar = ();
303	if (assoc_key_exists (a, lvar))
304	  continue;
305	uninited_vars = [uninited_vars, lvar];
306     }
307   if (length (uninited_vars))
308     uninited_vars = strcat ("variable ", strjoin (uninited_vars, ","), ";");
309   else
310     uninited_vars = "";
311
312   variable fmt = "private define %s (%s) { %s %s; }";
313   variable dummy = "__debugger_print_function";
314   variable fun = sprintf (fmt, dummy, strjoin (inited_vars, ","),
315			   uninited_vars, expr);
316   () = eval_in_frame (Current_Frame, fun, 0, print_fun);
317
318   % push values to the stack and call the dummy function
319   foreach lvar (inited_vars)
320     {
321	a[lvar];
322     }
323   () = eval_in_frame (Current_Frame, dummy, length (inited_vars), print_fun);
324}
325
326private define print_cmd (cmd, args, file, line)
327{
328   print_expr (&simple_print, args);
329   return 0;
330}
331
332private define pprint_cmd (cmd, args, file, line)
333{
334   print_expr (&pretty_print, args);
335   return 0;
336}
337
338private define list_cmd (cmd, args, file, line)
339{
340   variable dline = 5;
341   line = int (line);
342
343   if (Last_Cmd == cmd)
344     line = Last_List_Line + 1 + dline;
345
346   variable line_min = line - dline;
347   variable line_max = line + dline;
348
349   if (strlen (args))
350     {
351	line_min = integer (args);
352	line_max = line_min + 10;
353     }
354
355   display_file_and_line (file, line_min, line_max);
356   Last_List_Line = line_max;
357   return 0;
358}
359
360private define print_frame_info (f, print_line)
361{
362   variable info = _get_frame_info (f);
363   variable file = info.file;
364   variable function = info.function;
365   variable line = info.line;
366
367   if (function == NULL)
368     function = "<top-level frame>";
369
370   output("#%d %S:%d:%s\n", Max_Current_Frame-f, file, line, function);
371   if (print_line)
372     display_file_and_line (file, line, line);
373}
374
375private define up_cmd (cmd, args, file, line)
376{
377   if (Current_Frame == 1)
378     {
379	output ("Can't go up\n");
380	return 0;
381     }
382   Current_Frame--;
383   print_frame_info (Current_Frame, 1);
384   return 0;
385}
386
387private define down_cmd (cmd, args, file, line)
388{
389   if (Current_Frame == Max_Current_Frame)
390     {
391	output ("At inner-most frame\n");
392	return 0;
393     }
394   Current_Frame++;
395   print_frame_info (Current_Frame, 1);
396   return 0;
397}
398
399private define where_cmd (cmd, args, file, line)
400{
401   variable i = Current_Frame;
402   while (i > 0)
403     {
404	print_frame_info (i, 0);
405	i--;
406     }
407   return 0;
408}
409
410#ifexists fpu_clear_except_bits
411private variable WatchFPU_Flags = 0;
412#endif
413
414private define watchfpu_cmd (cmd, args, file, line)
415{
416#ifexists fpu_clear_except_bits
417   fpu_clear_except_bits ();
418   if (args == "")
419     {
420	WatchFPU_Flags = FE_ALL_EXCEPT;
421	output ("Watching all FPU exceptions:\n");
422	output (" FE_DIVBYZERO | FE_INEXACT | FE_INVALID | FE_OVERFLOW | FE_UNDERFLOW\n");
423	return 0;
424     }
425   WatchFPU_Flags = eval (args);
426   if (WatchFPU_Flags == 0)
427     {
428	output ("Watching FPU exceptions disabled\n");
429     }
430   return 0;
431#else
432   output ("watchfpu is not supported on this OS\n");
433   return 0;
434#endif
435}
436
437private variable Cmd_Table = Assoc_Type [Ref_Type];
438Cmd_Table["finish"] = &finish_cmd;
439Cmd_Table["next"] = &next_cmd;
440Cmd_Table["step"] = &step_cmd;
441Cmd_Table["break"] = &break_cmd;
442Cmd_Table["delete"] = &delete_cmd;
443Cmd_Table["cont"] = &continue_cmd;
444Cmd_Table["continue"] = &continue_cmd;
445Cmd_Table["watch"] = &watch_cmd;
446Cmd_Table["list"] = &list_cmd;
447Cmd_Table["pprint"] = &pprint_cmd;
448Cmd_Table["print"] = &print_cmd;
449Cmd_Table["exit"] = &exit_cmd;
450Cmd_Table["quit"] = &quit_cmd;
451Cmd_Table["up"] = &up_cmd;
452Cmd_Table["down"] = &down_cmd;
453Cmd_Table["where"] = &where_cmd;
454Cmd_Table["watchfpu"] = &watchfpu_cmd;
455
456% Aliases
457define sldb_add_alias (alias, cmd)
458{
459   if (0 == assoc_key_exists (Cmd_Table, cmd))
460     return;
461   Cmd_Table[alias] = Cmd_Table[cmd];
462}
463sldb_add_alias ("b", "break");
464sldb_add_alias ("c", "continue");
465sldb_add_alias ("d", "delete");
466sldb_add_alias ("h", "help");
467sldb_add_alias ("l", "list");
468sldb_add_alias ("n", "next");
469sldb_add_alias ("p", "print");
470sldb_add_alias ("pp", "pprint");
471sldb_add_alias ("q", "quit");
472sldb_add_alias ("s", "step");
473
474private define help_cmd (cmd, args, file, line)
475{
476   output ("Commands:\n");
477   variable cmds = assoc_get_keys (Cmd_Table);
478   cmds = cmds[array_sort(cmds)];
479   foreach cmd (cmds)
480     output (" %s\n", cmd);
481   return 0;
482}
483Cmd_Table["help"] = &help_cmd;
484
485private define sigint_handler (sig)
486{
487   Debugger_Step = STEP_STEP;
488   Stop_Depth = INT_MAX;
489   if (Depth == 0)
490     throw UserBreakError;
491}
492
493private variable Old_Sigint_Handler;
494private define deinit_sigint_handler ()
495{
496#ifexists SIGINT
497   signal (SIGINT, Old_Sigint_Handler);
498#endif
499}
500
501private define init_sigint_handler ()
502{
503#ifexists SIGINT
504   variable old;
505   signal (SIGINT, &sigint_handler, &old);
506   ifnot (_eqs(old, &sigint_handler))
507     Old_Sigint_Handler = old;
508#endif
509}
510
511private variable Last_Frame = -1;
512private variable Last_Function = NULL;
513
514private define debugger_input_loop ()
515{
516   variable max_frame = Max_Current_Frame;
517   %Last_Cmd_Line = NULL;
518   %Last_Cmd = NULL;
519   forever
520     {
521	variable e;
522	try (e)
523	  {
524	     %deinit_sigint_handler ();
525	     init_sigint_handler ();
526	     Debugger_Step = 0;
527
528	     if (Current_Frame > max_frame)
529	       {
530		  Max_Current_Frame = max_frame;
531		  Current_Frame = max_frame;
532	       }
533	     variable info = _get_frame_info (Current_Frame);
534	     variable file = info.file;
535	     variable line = info.line;
536
537	     variable cmdline, cmd, cmd_parm;
538	     forever
539	       {
540		  variable prompt = Prompt;
541#iffalse
542		  prompt = "Depth=${Depth},Stop_Depth=${Stop_Depth} $prompt"$;
543#endif
544		  cmdline = (@Debugger_Methods.read_input)(prompt, Last_Cmd_Line);
545		  if (cmdline == NULL)
546		    throw ReadError, "NULL input returned";
547
548		  cmdline = strtrim (cmdline);
549		  variable tokens = strtok (cmdline, " \t");
550		  if (length (tokens))
551		    {
552		       cmd = tokens[0];
553		       break;
554		    }
555	       }
556	     cmd_parm = substr (cmdline, 1+strlen(cmd), -1);
557	     cmd_parm = strtrim (cmd_parm, "\t ");
558
559	     if (0 == assoc_key_exists (Cmd_Table, cmd))
560	       {
561		  output("%s is unknown.  Try help.\n", cmd);
562		  Last_Cmd_Line = NULL;
563		  Last_Cmd = NULL;
564		  continue;
565	       }
566	     variable ret = (@Cmd_Table[cmd])(cmd, cmd_parm, file, line);
567	     Last_Cmd_Line = cmdline;
568	     Last_Cmd = cmd;
569	     if (ret) return;
570	  }
571	catch IOError:
572	  {
573	     sldb_stop ();
574	     vmessage ("Caught IOError exception -- stopping the debugger: %S",e.message);
575	     return;
576	  }
577	catch AnyError:
578	  {
579	     output("Caught exception:%S:%S:%S:%S\n", e.file, e.line, e.function, e.message);
580	  }
581     }
582}
583
584private define do_debug (file, line, bp_num)
585{
586   %output ("do_debug: file=%S, line=%S, fun=%S\n", file, line, bp_num);
587   Current_Frame = _get_frame_depth ()-2;
588   Max_Current_Frame = Current_Frame;
589   %vmessage ("Current_Frame=%d\n", Current_Frame);
590   % We do not want the debug_hook catching errors here
591   variable debug_hook = _set_debug_hook (NULL);
592   EXIT_BLOCK
593     {
594	if (Debugger_Step != STEP_EXIT)
595	  {
596	     () = _set_debug_hook (debug_hook);
597	     init_sigint_handler ();
598	  }
599     }
600
601   variable info = _get_frame_info (Current_Frame);
602   if (file == NULL)
603     {
604	file = info.file;
605	if (file == NULL)
606	  file = "???";
607     }
608   if (line == NULL)
609     line = info.line;
610
611   variable fun = info.function;
612   if (fun == NULL) fun = "<top-level>";
613
614   if ((file == "<stdin>"))% or (file == "***string***"))
615     {
616	Last_Frame = Current_Frame;
617	Last_Function = fun;
618	Debugger_Step = STEP_NEXT;
619	Stop_Depth = Depth-1;
620	return;
621     }
622   if (bp_num)
623     {
624	output ("Breakpoint %d, %s\n    at %s:%d\n", abs(bp_num), fun, file, line);
625     }
626   else if ((Last_Frame != Current_Frame) or (Last_Function != fun))
627     {
628	output ("%s at %s:%d\n", fun, file, line);
629     }
630   display_file_and_line (file, line, line);
631   Last_Frame = Current_Frame;
632   Last_Function = fun;
633
634   debugger_input_loop ();
635}
636
637private define bos_handler (file, line)
638{
639   %output ("bos: depth=%d, stop_depth=%d, fun=%S\n", Depth,Stop_Depth,_get_frame_info(-1).function);
640   variable pos = make_breakpoint_name (file, line);
641   variable bp = Breakpoints[pos];
642
643   if (bp)
644     {
645	if (bp < 0) Breakpoints[pos] = 0;   %  clear temporary breakpoint
646	do_debug (file, line, bp);
647	return;
648     }
649
650   if (Depth > Stop_Depth)
651     return;
652
653   if (Debugger_Step == 0)
654     return;
655
656#iffalse
657   if (Debugger_Step == STEP_FINISH)
658     return;
659
660   if (Debugger_Step == STEP_NEXT)
661     {
662	if (Depth > Stop_Depth)
663	  return;
664     }
665#endif
666   do_debug (file, line, bp);
667}
668
669% end of statement handler: tracks the recursion depth,
670% to be able to step over function calls (using 'Next' Command)
671private define eos_handler()
672{
673#ifexists fpu_clear_except_bits
674   if (WatchFPU_Flags)
675     {
676	variable bits = fpu_test_except_bits (WatchFPU_Flags);
677	if (bits)
678	  {
679	     variable info = _get_frame_info (-1);
680	     variable str = String_Type[0];
681	     if (bits & FE_DIVBYZERO) str = [str,"FE_DIVBYZERO"];
682	     if (bits & FE_INEXACT) str = [str,"FE_INEXACT"];
683	     if (bits & FE_INVALID) str = [str,"FE_INVALID"];
684	     if (bits & FE_OVERFLOW) str = [str,"FE_OVERFLOW"];
685	     if (bits & FE_UNDERFLOW) str = [str,"FE_UNDERFLOW"];
686	     output ("*** FPU exception bits set: %s\n", strjoin(str, ","));
687	     output ("Entering the debugger.\n");
688	     fpu_clear_except_bits ();
689	     do_debug (info.file, info.line, 0);
690	  }
691     }
692#endif
693   %output ("eos: depth=%d\n", Depth);
694}
695
696private define bof_handler (fun, file)
697{
698   %output ("Entering BOF: %S, %S, %S", fun, file, line);
699   Depth++;
700
701   variable bp = Breakpoints[fun];
702   if (bp)
703     {
704	output ("Breakpoint %d, %s\n", abs(bp), fun);
705	if (bp < 0) Breakpoints[fun] = 0;   %  clear temporary breakpoint
706	Debugger_Step = STEP_NEXT;
707	Stop_Depth = Depth;
708     }
709}
710
711private define eof_handler ()
712{
713   %output ("Leaving EOF");
714   Depth--;
715   if (Debugger_Step)
716     {
717	if (Debugger_Step == STEP_FINISH)
718	  {
719	     if (Depth == Stop_Depth)
720	       {
721		  Debugger_Step = 0;
722		  %variable info = _get_frame_info (_get_frame_depth ()-2);
723		  %do_debug (info.file, info.line, 0);
724		  do_debug (NULL, NULL, 0);
725	       }
726	  }
727	if ((Debugger_Step == STEP_NEXT) and (Stop_Depth > Depth))
728	  Stop_Depth = Depth;
729     }
730}
731
732private define debug_hook (file, line)
733{
734   if (Startup_PID != getpid())
735     return;
736
737   %variable file = e.file, line = e.line;
738   variable e = __get_exception_info ();
739   output ("Received %s error.  Entering the debugger\n", e.descr);
740   check_breakpoints ();
741   do_debug (file, line, 0);
742}
743
744define sldb_enable ()
745{
746   ()=_set_bos_handler (&bos_handler);
747   ()=_set_eos_handler (&eos_handler);
748   ()=_set_bof_handler (&bof_handler);
749   ()=_set_eof_handler (&eof_handler);
750   ()=_set_debug_hook (&debug_hook);
751
752   check_breakpoints ();
753   Depth = 0;
754   Debugger_Step = STEP_STEP;
755   init_sigint_handler ();
756   _traceback = 1;
757   _bofeof_info = 1;
758   _boseos_info = 3;
759}
760
761% Usage Forms:
762%   sldb ();
763%   sldb (file);
764%   sldb (file, ns);
765% The namespace semantics are the same as that of require.
766define sldb ()
767{
768   Startup_PID = getpid ();
769   sldb_initialize ();
770
771   sldb_enable ();
772   if (_NARGS == 0)
773     {
774	Current_Frame = _get_frame_depth ()-1;
775	Max_Current_Frame = Current_Frame;
776	debugger_input_loop ();
777	return;
778     }
779   variable args = __pop_args (_NARGS);
780   require (__push_args (args));
781#iffalse
782   variable ns = current_namespace ();
783   if (_NARGS == 2)
784
785     ns = ();
786   variable file = ();
787
788   if (ns == NULL)
789     () = evalfile (file);
790   else
791     () = evalfile (file, ns);
792#endif
793}
794
795% remove bos and eos handlers.
796define sldb_stop ()
797{
798   ()=_set_bos_handler (NULL);
799   ()=_set_eos_handler (NULL);
800   ()=_set_bof_handler (NULL);
801   ()=_set_eof_handler (NULL);
802   ()=_set_debug_hook (NULL);
803   deinit_sigint_handler ();
804   _bofeof_info = 0;
805   _boseos_info = 0;
806   Debugger_Step = STEP_EXIT;
807}
808
809provide ("sldbcore");
810