Basic debugging; a structured approach. Reginald Gates.
A Structured Approach
All of us want to write error-free programs, but since we are human, we all need to learn debugging skills. What follows is a structured approach to locating and correcting program problems. Given the nature of programs and programmers, this cannot be an exhaustive treatment of the subject, but it will give us a place to start. We will begin by looking at the debugging tools we can use, then at the analysis of the error itself, and finally at the types of bugs we are likely to encounter.
What Help Do We Have?
The first question we should answer is: What resources are available from the system to help us? Every compiler or interpreter comes with certain features designed to make debugging easier. Below are some of the debugging aids available in most Basics. Although the implementation of the feature may vary with the version of Basic you are using, the function still should be present.
The BREAK function is tied to key input. The Basic interpreter monitors the keyboard during execution, and, if the key corresponding to the BREAKf unction is depressed, the interpreter will halt execution and tell us what the line number is being executed. (Break in line 2055 is a typical message.)
This function is implemented by CONTROL-C in some Basics. We should also be aware that some systems permit the program being executed to disable this feature.
The most important thing about BREAK is that when it is invoked, control returns to the user, but the values of the program variables are left as they were when the BREAK was executed. This enables us to use the next feature extensively.
Most Basics permit a PRINT statement to be entered as a command, i.e., to be entered whenever the Basic interpreter is waiting for a statement from the user. The operands for the PRINT command can be any of the variables for the program being tested.
This enables us to examine the contents of both string and numeric variables after the Break feature has been used.
If a STOP statement is placed in the program, Basic will cease execution when it encounters that statement and return control to the user. The user can then USE PRINT to examine the variables, much as was done after the BREAK feature.
Since STOP is a statement, it can usually be coded in an IF test. For example, if the program seems to be having trouble in a certain routine whenever X is negative, we could code.
1022 IF X < 0 THEN STOP
This command permits us to resume execution after a STOP statement or BREAK command. When combined with the STOP and PRINT features. CONTINUE is a very useful debugging tool. For example, we can halt a loop on every iteration, examine the variables, and then continue execution. See Listing 1 for an example.
When the STOP in statement 125 2s executed, we can Print D, X, and Y to see if routine 2000 was successful. If so, we can say CONTINUE to go to the next value of D. In this manner we can step through the loop, looking for a D that produces an invalid X and Y.
When this feature is invoked, Basic displays the line number of each program statement as it is executed. In this manner, we can tell exactly which program statements are being used and in what sequence.
Use of the TRACE feature does have a few drawbacks. For one thing, the TRACE line numbers are interspersed with material displayed by the program on the screen. The resulting display can be very confusing. Also, the volume of output is high, so the screen scrolls fairly rapidly and we may miss important information.
Most Basics allow TRACE to be used as both a command and a statement, so that TRACE ON can be placed within the body of a program or coded in an IF statement.
At least one Basic is set up so that the depression of CONTROL-A freezes the display. When the key is released, execution resumes. This permits us to look at a screen, note any important data, and then continue.
Now that we know what resources we have, we are ready to turn to the most important step in debugging--defining the problem.
What Is The Problem?
The problem with the program must be defined as thoroughly as possible. A good definition answers at least three questions:
Generating this definition is a surprisingly difficult task and many people are tempted to bypass this step and start to look at the code.
If we cannot describe the problem in a short, written paragraph that includes the three elements above, we must continue our analysis. For one thing, if we don't know precisely what the problem is, how will we know when (or if) it is corrected? How will we knew which area of code to look at? And how can we generate an appropriate set of test data if we can't describe what we are to test? An exact problem definition is a pre-requisite to any debugging.
Below are three versions of a bug definition recently corrected in an on-line order system that I was working on. Once the third statement had been developed, the correction was relatively easy.
Defining the problem thoroughly can even suggest a new approach or a novel solution. When I first started in programming, our group had a problem with a daily program that tracked drawings from station to station in a large aerospace company.
Whenever there was a tape drive failure while this job was active, it would release all the drawings we had. Rerunning the job always corrected the situation, but no one could figure out how the tape drive problem could cause the program failure.
We corrected this bug by adding a special counting routine to the program. If this routine detected that all the drawings input had been released, it produced a message to the operator instructing him to re-run the job when all the tape drives were operational. Some pursis may object that this was not a real solution, but it did eliminate many late night phone calls.
Once the problem has been defined, it is very helpful to have an idea of what we are looking for before we start to study the code. Accordingly we will now look at the ways in which errors can be classified.
What Kind Of Error?
The coding within the program can be thought of as a series of interrelated structures or units, each of which follows this general pattern:
1. Sequential processing
2. Conditional or loop control
3. Repetitive processing for 2 above.
For example, in the short program in Listing 2, statements 10 and 60 are sequential processing/initialization, statement 20 controls a loop, and 30 and 40 belong to class 3 above.
We can classify program bugs according to this same general scheme. This will help us in determining where (in what type of coding structure) to look for the error.
1. Sequential Processing/Initialization Errors. In this type of error variables are not set properly, usually prior to executing a set of repetitive processing statements. The results of the repetitive processing are off by a fixed amount, or major branches in the logic are not taken. For example, the code in Listing 3 seems to contain a simple programming error in statement 500.
Apparently there is some special processing to be done when the first negative value of the A array is encountered. However, because W1 is set to 1 in statement 500, the routine at 2000 is never executed. The TRACE feature will easily show us that the 2000 routine is never invoked, or we may place a STOP as the first statement in the 2000 routine. Since the routine will never be entered, the STOP statement will never be encountered, and we may deduce that there was no negative A(I) or that W1 was not set correctly.
2. Repetitive Errors. Errors of this type are characterized by the resulting data being wrong "in the same way' for a set of input. For example, suppose the following code was used round dollar amounts in an array.
100 FOR I = 1 TO N1
100 A(I) = INT(A(I)+.05)
120 NEXT I
Since all entries in the array that have a cent amount of less than 95 cents will not be rounded properly, we will expect that some statement in the repetitive processing is incorrect.
3. Loop Control or Conditional Errors. These can be simple coding logic errors or some of the most difficult errors to debug. One of the most simple and yet most common conditional errors is shown below. It involves mixing an OR test with a not equal conditional.
1000 IF A < > 1 OR A < > 2 THEN GOSUB 2000
Routine 2000 will always be executed, regardless of the value of A. If A is 1, it is not equal to 2, so the GOSUB is executed. IF A is 2, it is not equal to 1, and the GOSUB is executed. Again, the TRACE feature will show us this type of problem, or we might put a STOP statement at the beginning of the 2000 routine. When the program halted, we could then print the value of A and observe that we were entering the subroutine when we shouldn't be (when A is 1 or 2).
A more difficult problem occurs when control is lost in a FOR-NEXT loop. Usually an infinite loop results and the program repeats the same set of instructions over and over. This results in no response or output to the screen for a long time or in the same response being displayed over and over again.
Using the BREAK feature will stop the program in one of the instructions (not necessarily the FOR or the NEXT instruction). We may then print the variables involved in the nearby statements and try to determine why we are not exiting the loop. Usually we will find that one variable is controlling the repetition and that this variable is modified incorrectly by one of the statements or subroutines in the loop. The code in Listing 4 will produce an infinite loop of this type.
If C1 is greater than 25, the exit condition in 300 will never be satisfied, since the subroutine at 5000 will leave I set at 25 when it exist, regardless of the value of I when the subroutine was entered. Of course, this is easy to see when only the relevant routines are listed. It is much harder to find when the 300 and 4000 subroutines must also be examined.
The TRACE feature will show us what statements are executed, but it will not give us an idea of where I is being reset. The best approach might be to put a STOP statement after each statement that invokes a subroutine in the 300 area. We can then display I at each point and isolate the offending subroutine.
Although there is no universally useful procedure for debuggin a program, as you can tell by the number of times we used "usually' or "normally' in the examples above, the type of structured approach that we have discussed can be very helpful.
Remember, know your tools, define the problem, classify the error, and good luck.
Table: Listing 1.
Table: Listing 2.
Table: Listing 3.
Table: Listing 4.