Debugging in Yacas
Introduction
When writing a code segment, it is generally a good idea to separate the
problem into many small functions. Not only can you then reuse these
functions on other problems, but it makes debugging easier too.
For debugging a faulty function, in addition to the usual trial-and-error method and the "print everything" method, Yacas offers some trace facilities. You can try to trace applications of rules during evaluation of the function (TraceRule(), TraceExp()) or see the stack after an error has occurred (TraceStack()).
There is also an interactive debugger, which shall be introduced
in this chapter.
Finally, you may want to run a debugging version of Yacas. This
version of the executable maintains more information about
the operations it performs, and can report on this.
This chapter will start with the interactive debugger, as it
is the easiest and most useful feature to use, and then proceed
to explain the trace and profiling facilities. Finally, the
internal workings of the debugger will be explained. It is highly
customizable (in fact, most of the debugging code is written in
Yacas itself), so for bugs that are really difficult to track
one can write custom code to track it.
The interactive command line debugger
Yacas comes with a full interactive command line based debugger.
The evaluator has hooks so that a programmer can customize evaluation.
A very advanced use is to roll a custom debugger for a specific bug.
This section will not go into that, but show the current facilities
offered by the interactive command line debugger.
An example
Let us start with a simple example. Suppose we ran into the problem where
a function called returns an error:
In> Contains(a,{a,b,c})
In function "Head" :
bad argument number 1 (counting from 1)
The offending argument list evaluated to a
CommandLine(1) : Argument is not a list
|
and suppose we want to examine what went wrong. We can invoke the debugger
by calling Debug, with the expression to debug as argument:
In> Debug(Contains(a,{a,b,c}))
>>> Contains(a,{a,b,c})
Debug>
|
The screen now shows the expression we passed in, and a Debug> prompt.
The debugger has essentially been started and put in interactive mode.
This would for instance be a good moment to add breakpoints. For now, we
will just start by running the code, to see where it fails:
Debug> DebugRun()
DebugOut> False
CommandLine(1) : Argument is not a list
>>> Head(list)
Debug>
|
The interpreter runs into a problem and falls back to the interactive
mode of the debugger. We can now enter expressions on the command line,
and they will be evaluated in the context the interpreter was stopped
in. For instance, it appears the interpreter tried to evaluate Head(list),
but list does not seem to be a list. So, to check this, we examine the
contents of the variable list:
Indeed list is bound to a, which is not a list. Examining all the
local variables on the stack, we find:
Debug> DebugLocals()
*************** Current locals on the stack ****************
list : a
element : {a,b,c}
result : False
DebugOut> True
|
So it seems we swapped the two arguments, as the values of list and
element should be swapped. We first drop out of the debugger, and
then try the call with the arguments swapped:
Debug> DebugStop();
DebugOut> True
CommandLine(1) : Debugging halted
In> Contains({a,b,c},a)
Out> True;
|
so we found the problem.
Debugging functions supported
After the debugger has been invoked, the following commands can be used
in interactive mode while debugging:
- DebugRun() - continue evaluating the expression
- DebugStep() - perform one step, until the interpreter finds the next (sub) expression to evaluate
- DebugStepOver() - perform one step, but just evaluate the current expression, not tracing into it.
- DebugStop() - completely halt evaluation
- DebugVerbose(verbose) - turn on trace-like output to the console
- DebugAddBreakpoint(name) - set a breakpoint at a certain function. The interpreter
will now stop at the point where the arguments to a function need to be evaluated.
- DebugBreakIf(predicate) - halt when a certain predicate becomes true. This is useful for designing a custom break point (eg. stop when a
certain local variable gets some value). The predicate is removed again
after it has fired the first time.
- BreakpointsClear() - remove all breakpoints (including the custom break point predicate).
- DebugLocals() - show the currently available local variables
- DebugCallstack() - show the current function call stack
Stepping through the files while debugging
The debug version of Yacas (which can be obtained by
passing the --enable-debug parameter to ./configure)
keeps track of where each function was defined. For each
object in the tree kept in memory it maintains a filename
and line number. This is used by the debug code. When using
the debug version, the part of the file that is currently
being executed will be shown. In front of the file lines
there can be a sign ">". This points at the current line
being executed. In addition, one can put breakpoints at
specific lines in specific files. These will be shown
with a "*" in front of the lines.
The additional commands available when the debug version
of the Yacas executable is used are:
- DebugShowCode() - shows the current block of code being executed
- DebugBreakAt(file,line) - add a breakpoint at a specific point in a specific file. The file must be entered as it would have been entered when loading it. A user-defined file would go by "userdef.ys", whereas one of the Yacas scripts would be for example "linalg.rep/code.ys"
- DebugRemoveBreakAt(file,line) - remove a breakpoint
The trace facilities
The trace facilities are:
- TraceExp : traces the full expression, showing all calls to user- or system-defined functions, their arguments, and the return values. For complex functions this can become a long list of function calls.
- TraceRule : traces one single user-defined function (rule). It shows each invocation, the arguments passed in, and the returned values. This is useful for tracking the behavior of that function in the environment it is intended to be used in.
- TraceStack : shows a few last function calls before an error has occurred.
- Profile : report on statistics (number of times functions
were called, etc.). Useful for performance analysis.
The online manual pages (e.g. ?TraceStack) have more information about the use of these functions.
An example invocation of TraceRule is
In> TraceRule(x+y)2+3*5+4;
|
Which should then show something to the effect of
TrEnter(2+3*5+4);
TrEnter(2+3*5);
TrArg(2,2);
TrArg(3*5,15);
TrLeave(2+3*5,17);
TrArg(2+3*5,17);
TrArg(4,4);
TrLeave(2+3*5+4,21);
Out> 21;
|