CS 307: Debugging

Debugging programs is an important skill for a computer scientist. Here are some hints about debugging:

  1. Document clearly what each function should do. Write a comment (after a semicolon) that states what the function does. Write a comment describing each argument, unless it is obvious. Write a comment with an example of a call to the function.
    ; Compute the value of an investment of a fixed contribution for a
    ;    given number of time periods at a specified interest rate.
    ; contribution = amount added at the end of a time period
    ; time         = number of time periods
    ; interest     = interest paid per time period, e.g. 6% = 0.06
    ; Example: (invest 1040 60 0.06)  ; $1040 per year for 60 years at 6%
    (define (invest contribution time interest) ...)
    
    These comments are a specification of your program. One source of bugs is an unclear (poorly understood, unwritten) specification. Sometimes, a clear specification will make it obvious how to write the program. One of the best ways to debug is not to write bugs; a good specification helps.

  2. Test the function on an easy case first. Often, the easy case is the base case of a recursive function. Make sure the answer is correct for the easy case. Then try a slightly harder case -- one for which you understand the answer.
    > (invest 100 0 0.06)
    0
    
    > (invest 100 1 0.06)
    100.
    
    > (invest 100 2 0.06)
    206.
    
    Investing $100 zero times gives zero. Investing $100 at the end of one time period leaves us with the given $100. Investing $100 for two time periods gives us interest of $6 on $100 during the second time period.

    If your function works on the base case and a few more cases, it probably works for all cases; if not, it will be easier to understand and fix the function when the case is an easy one.

  3. Trace your functions. Use (trace fn ) to trace the function fn. You can turn off a trace with (untrace fn). The trace will show the arguments of a function on entry and the result on exit. The entry and exit are matched by vertical alignment of the trace printouts.

  4. As soon as you see a bug, track it down and fix it. Develop a keen eye for anything that looks unusual in your output; if you see something abnormal, focus on it.

  5. Test your functions one at a time. This is easy in Lisp: simply type in a call to a function with test arguments, even if it is not the ``main'' function. It is easy to test a single function in isolation, but hard to test a large system.

  6. Use rapid prototyping and evolve your programs: write a program that does part of the job, test it, then expand it. When you get part of the program working, you can expand on that model; this is better than repeating the same mistake many times in your code before doing any testing.

  7. If your program ``cannot possibly be doing what it is doing'', the bug is probably something that you are not looking at. Take a wider view and look at other things.

  8. Instrument your code with debugging printouts: print out internal values and look for abnormalities. Sometimes students stay up all night trying to reason about why their program does not work, when a simple printout would make the problem obvious.

    You can detect internal errors and stop your program (and then get variable values) using an (error   "message") call.

       (if stopping-condition
           (error "The elusive bug has occurred!"))
    

  9. When you get an error break, DrScheme can provide additional information: