However, in my opinion, there are circumstances under which debuggers are useful:
- when working your way through legacy code,
- when encountering a strange bug that is completely beyond your understanding,
- when experimenting with a new technology.
At every function call you face a choice: to step into the function or not to step into the function. If you optimistically don't step into it and find that its result is not what you expected, you have to do it all over again: setting a new breakpoint, restarting the program (or test suite)...Tedious!
A Proposal
Why do debuggers work this way? Wouldn't it be much more useful if debuggers could just show what goes on inside a function, without requiring you to step through the code again and again?To explore the idea, I implemented a prototypical "source code annotating debugger"1 for a subset of the programming language Oz. I'm using Oz as a functional programming language here, but it does in fact support multiple styles of programming in a harmonious way.
Example
Let's look at a (somewhat contrived) example which contains a well-hidden bug:declare
fun {RemoveEvenNumbers Xs}
case Xs of X|Xr then
if {IsOdd X} then
X|{RemoveEvenNumbers Xr}
else
{RemoveEvenNumbers Xr}
end
[] nil then nil
end
end
fun {Sum Xs}
{FoldL Xs Number.'+' 0}
end
fun {SumOfOddNumbers Xs}
{Sum
{RemoveEvenNumbers Xs}
}
end
proc {Main}
{System.show {SumOfOddNumbers [~3 ~2 ~1 0 1]}}
end
in
{Main}
(The example has to be executed in the "OPI", i.e. from within Emacs in "Oz mode". Don't forget to save the buffer first because the debugger needs a buffer filename to work correctly.
The Oz syntax may look unfamiliar first, but it is in fact not much different from other functional languages.fun {Name FormalArg1 FormalArg2) ... end
defines a function "Name" with two arguments.{Name ActualArg1 ActualArg2}
calls the defined function.
Thecase
statement is a pattern matching construct andX|Xr
constructs a list with the headX
and the tailXr
.)
The example program is intended to calculate and display the sum of a list of numbers, only taking into account the odd numbers. When running the code (with Mozart version 1.3.2), the result is:
1
. Huh? It should of course be -3 + -1 + 1 = -3
.With the cursor on the call to
SumOfOddNumbers
, the debugger is started with a keyboard shortcut (M-Shift-t). Its output is the following annotated code, shown in a new Emacs buffer:fun {SumOfOddNumbers Xs}
{Sum
{RemoveEvenNumbers Xs} %%> Xs:[~3 ~2 ~1 0 1] {RemoveEvenNumber..}:[1]
} %%> {Sum {Remov..}:1
end
The problem seems to be the function
RemoveEvenNumbers
. Its result is the single-element list [1]
, which is certainly not expected.We move the cursor to the call to
RemoveEvenNumbers
and press M-Shift-t again. The debugger will annotate the source code of RemoveEvenNumbers
as if it was executed with the current parameters bindings:fun {RemoveEvenNumbers Xs}
case Xs of X|Xr then %%> Xs:[~3 ~2 ~1 0 1] <<X|Xr>>:success
if {IsOdd X} then %%> X:~3 {IsOdd X}:false
X|{RemoveEvenNumbers Xr}
else
{RemoveEvenNumbers Xr}%%> Xr:[~2 ~1 0 1] {RemoveEvenNumber..}:[1]
end %%> <<if {IsOdd X} t..>>:[1]
[] nil then nil
end %%> <<case Xs of X|X..>>:[1]
end
Curiously, the system function
IsOdd
returns false
for the input value ~3
(Oz notation for "-3"). This is unexpected! It looks like we found an error in the system libraries.Let's check the source code of
IsOdd
(again with M-Shift-t):fun {IsOdd X} X mod 2 == 1 end %%> X:~3 << X mod 2>>:~1 <<X mod 2..>>:false
Finally, we found the real bug.
IsOdd
only works correctly for non-negative numbers because X mod 2
is negative for odd negative values of X. The underlying problem is that in Oz the modulo operation returns a negative result for negative dividends (as in many other languages), while in math, the result is always positive.(In reality, I did not find this bug, but another Oz user did and the bug is already fixed in the subversion repository.)
Conclusion
With the standard Oz debugger Ozcar (which is very usable), it takes about 12 key strokes and some mouse work to find out the reason for the bug in this small example. Using the alternative approach, it takes two key strokes plus some navigating in Emacs.I believe this is a significant improvement. The biggest advantage is that you can easily inspect the values of all variables and the result of function calls at every place within the debugged function - just by looking at the annotated source code.
Limitations
The current implementation has some serious limitations. Some of these can be overcome by more sophisticated implementations, with some I'm not so sure:- The display space for annotations is limited. Large data cannot be shown completely. This can be resolved by an implementation which allows to explore the annotations interactively.
- The debugger shows inconsistent results if the 'world' changes between executions, e.g. if global state is involved or external files are read and written. To solve this, one needs a debugger which remembers the state of all variables, along the complete callgraph, such that multiple executions are not necessary. For large programs, this might be impractical.
- One of the features of Oz which are not supported, are
for
loops. They are in fact difficult to support because it would be necessary to annotate the code with the data of every single run of the loop.
Recursive function likeFoldL
are less problematic. You can simply press M-Shift-t again and again if you want to explore the nested calls to such a function.
The proposed approach to debugging therefore seems to be more useful for (mostly) functional programming language like Haskell or Oz than for imperative languages like C++, which very much depend on the use of loops.
1The prototypical implementation of the source-code annotating debugger for Oz can be downloaded here.
4 comments:
Of course, being able to annotate the source requires (more or less) a log of all state changes during program execution.
Once you have that, you get something far more useful - a debugger that allows you to step forwards and backwards in time.
This addresses the issue of limited annotation space - the programmer just gets to pick which particular information he's interested in.
Oh, in case you're interested, here's a short paper on backtracking debuggers.
Robert, I did in fact think of time-travelling debuggers when I wrote about the limitations, and I think the idea is wonderful.
The one advantage of my approach is, in my opinion, the minimal required interaction from the programmer.
Thanks for the link!
huh, hat? http://www.haskell.org/hat/tutorial1.html
Post a Comment