The problem of debugging a program is distinct from the issues raised in the "Handling Errors" chapter. The "Handling Errors" chapter is based on the premise that the programmer is satisfied that the program works as it should, and that it then should be made as foolproof as possible. This could be construed as putting the cart before the horse--before you can make a program foolproof, you must get it to run correctly in the first place. One of the key characteristics of a "bug" is that it doesn't necessarily have to cause an error condition to occur--it only has to cause your program to give a wrong answer. This chapter deals with the methods available to diagnose problems in logic and semantics.
Naturally, the ideal way to debug a program is to write it correctly the first time through, and all programmers should strive constantly to achieve this goal. Hopefully, the techniques that have been been discussed in this manual will help you get a little closer to this goal. The practice of writing self-documenting code and designing programs in a top-down fashion should help immensely.
Aside from recommended methods of writing software, the computer itself has several features which aid in the process of debugging.
One of the pleasing characteristics of your computer is that its keyboard is "live" even during program execution. That is, you can issue commands to the computer while it is running a program the same way that you issue commands to it while it is idle. For instance, you can add two numbers together, examine the catalogue of the disk currently installed in the drive, list the running program to a printer, scroll the CRT alpha buffer up and down, or output a command to a function generator over HP-IB. Practically the only thing you can't do from live keyboard while a program is running is write or modify program lines, or attempt to alter the control structures of the program. (A complete list of illegal keyboard operations is given a little later on.)
By way of illustration, key in the following program, press [RUN] ([f3] in the System, User 1, and User 2 menus of an ITF keyboard), and then execute the commands shown underneath the listing.
10 FOR I=1 TO 1.E+6
20 NEXT I
30 END
CAT
2+2
SQR(6^2+17.2^2)
PRINT "THE QUICK BROWN FOX"
TIMEDATE
Now, this program will take a fair amount of time to complete (on the order of minutes), so to find out how far the program has gone, look at the value of the variable I. Type: [I] [Return] or [ENTER].
The current value of I
will be displayed at the bottom
of the screen.
If you don't want to wait for the program to go through all one million
iterations, you can merely change the value of I
by entering:
I=999000
Thus, we have seen that live keyboard can be used to examine and/or change the contents of the program's variables.
One aspect of live keyboard to be aware of is that the computer will only recognize variables that exist in the current program environment. For instance, suppose that we change our example program to call a subprogram inside the loop.
10 FOR I=1 TO 1.E+5
15 CALL Dummy
20 NEXT I
30 END
40 SUB Dummy
50 FOR J=1 TO 10
60 NEXT J
70 SUBEND
While this program is running and you try and test the variable
I
from the keyboard, chances are that you will only get
a message saying that I
doesn't exist in the current
context--most of the time will be spent in the subprogram. On the other hand,
if you test the value of J
, it is highly likely that
you will get an answer.
Similarly, operations like ASSIGN and ALLOCATE, which are declarative types
of statements, must use variables that are already known to the current
environment when they are executed from the keyboard. For example, in the
following program, it is perfectly legal to perform the operation
ASSIGN @Dvm TO *
from the keyboard, although it is not
legal to perform ASSIGN @File TO "DATA"
from the keyboard.
1 ASSIGN @Dvm TO 724
10 FOR I=1 TO 1.E+5
20 NEXT I
30 END
Live keyboard operations are allowed to use variables already known by the running program. Live keyboard operations are not allowed to create variables.
Although the GOTO and GOSUB commands are illegal from the keyboard, it is perfectly legal to call subprograms from the keyboard. The parameters that are passed must either be constants or must be variables that exist in the current context. Also, the program in memory must be able to pass pre-run without errors.
Here is an example:
10 FOR I=1 TO 1E5
20 NEXT I
30 END
40 SUB Gather(INTEGER X)
50 OPTION BASE 1
60 DIM A(32)
70 CREATE BDAT "File"& VAL$(X),1
80 ASSIGN @Dvm TO 724
90 ASSIGN @File TO "File"& VAL$(X)
100 OUTPUT @Dvm;"N100S"
110 ENTER @Dvm;A(*)
120 OUTPUT @File;A(*)
130 PRINT A(*),
140 SUBEND
150 DEF FNPoly(X)
160 RETURN X^3+3*X^2+3*X+X
170 FNEND
By executing CALL Gather(1)
from the keyboard, the main
program will be suspended while the subprogram is called, at which time a
1 record file will be opened, 32 readings will be taken from the voltmeter
and stored in the file, and the readings will be printed on the screen. Then
main program execution will resume where it left off.
Similarly, by executing FNPoly(1)
, the value of the
polynomial will be computed for X=1 and the answer (8) will be displayed
at the bottom of the screen.
You can also pause a program from the keyboard using the [PAUSE] ([Break]) key.
You may subsequently continue program execution:
CONT [Return] or [ENTER]
or
CONT 100 [Return] or [ENTER]
or
CONT Line_label [Return] or [ENTER]
Note that a program which has been edited cannot be continued.
Here is a list of commands which may not be executed from the keyboard while a program is running, although they may be executed from the keyboard if the computer is idle:
CHANGE | FIND | SCRATCH |
CONT | GET | SCRATCH A |
COPYLINES | LOAD | SCRATCH BIN |
DEL | MOVELINES | SCRATCH C |
EDIT | RUN | SYSBOOT |
When debugging a program, and you think that the problem may be that you misspelled a variable name, you can use the XREF command to alphabetically list all variable names. This listing will also contain the line numbers where the variables were used, to help you locate any problems caused by misspelling or using the wrong variable.
Another way of using a cross-reference listing is when you need to find every place a particular variable name is used, but the system (and therefore the FIND command) is not available. It is often advisable to generate a cross reference at the end of a hard-copy (printer) listing of a large program. This information makes finding every occurrence of a variable much easier.
The following XREF command prints a cross-reference listing on the default PRINTER IS device:
XREF
The next command sends a cross-reference to device selector 701:
XREF #701
Here is an example program, with a corresponding cross reference.
10 ! File "DoKeyFile"
20 DIM Key_value$[160]
30 INTEGER Key_number
40 CREATE BDAT "SOFTKEYS",3
50 ASSIGN @Keys TO "SOFTKEYS"
60 FOR I=0 TO 9
70 READ Key_number,Key_value$
80 OUTPUT @Keys;Key_number,Key_value$
90 NEXT I
100 ASSIGN @Keys TO *
110 LOAD KEY "SOFTKEYS"
120 ! ---- Key Data ----------------------
130 DATA 8,"work!",5,"that",1,"See?",4,"you"
140 DATA 2,"I",3,"told",7,"would",6,"this"
150 END
Now generate a cross reference of the identifiers in the program:
XREF [Return] or [ENTER]
The following results are generated:
>>>> Cross Reference <<<<
* Numeric Variables
I 60 90
Key_number 30 <-DEF 70 80
* String Variables
Key_value$ 20 <-DEF 70 80
* I/O Path Names
@Keys 50 80 100
Unused entries = 1 (on Series 200/300/400)
Unused entries = 4 (on Series 700)
This is not an exhaustive list of XREF outputs, since there were no COM blocks, subprogram calls, line labels, etc. However, it does give an idea of the general format of a cross-reference listing. (For a complete description of XREF listings, see the HP BASIC Language Reference.)
Note the <- DEF
which appears in some of the line-number
lists; this symbol appears when:
At the end of each context, a line is printed that begins with:
Unused entries =
The number of "unused entries" deals with the internal workings of the system. It tells how many symbol table entries are available:
This is a count of the symbol table entries which have been marked by pre-run as unused. Unreferenced symbol table locations which have not yet been marked unused by pre-run processing will show up in the lists of identifiers with empty reference lists. Note that a distinction is made here between unused and unreferenced.
Pre-run will convert unreferenced symbol table entries (entries which are defined by the system but not used by a variable in the program) into "unused" entries. Unreferenced entries can arise because you changed your mind about a variable's name or corrected a typing error (once the system reserves space for a symbol table entry, this space is dedicated to the purpose of storing symbols until the corresponding context is destroyed, such as with SCRATCH). "Unreferenced entries" can also arise in syntaxing some statements where a numeric variable name which becomes a line label or a subprogram name is created. Also, REN (renumber) can cause line numbers to merge if you have unsatisfied line-number references. This shows up in the cross-reference as separate (but adjacent) entries for the multiple symbol table entries for the line number.
Let's go through an example to make this completely clear. At power-up the system creates an empty symbol table. For Series 200/300/400 computers, the initial symbol table has space for 5 entries. For Series 700 computers, the initial symbol table has space for 8 entries. At power-up, doing an XREF will show the number of currently unused entries.
Next, type the following program:
10 A=1
20 B=2
30 C=3
Executing an XREF after entering that program will show three variables,
each occurring in one line, and the unused entry count will be reduced by
three. If you fill all the unused entries by adding more lines and then execute
an XREF, it will show Unused entries = 0
. Now, if you
add one more line, the system will create some additional symbol table entries.
On Series 200/300/400 computers, the required number of entries are allocated
and five additional entries; on Series 700 computers, the required number
of entries are allocated and eight additional entries.
Next, delete lines 10, 20, and 30 from the program above by pressing the [Delete line] key or by executing
DEL 10,30
Now an XREF will show the three variables, each with an empty reference list, but with the same number of unused entries. These results occur because the symbol table entries are unreferenced, but have not been reclaimed as unused.
Next, store the following program line (as the only line):
10 END
and run the program.
Now, executing an XREF will show all of the entries as unused.
When you execute a SCRATCH, the initial, power-up state of the symbol table is restored, that is, 5 empty locations (Series 200/300/400) or 8 empty locations (Series 700).
As another program example, enter the following lines:
10 GOTO A
20 A:END
Then execute XREF. This will show a numeric variable A (which is an artifact
of the syntaxing process) and the line label A (referenced in two places).
Running this program will cause pre-run to recognize that there is no occurrence
of a numeric variable A in the program and reclaim the space for future use,
converting it back into an "unused entry". Variables which are defined in
the program are considered "referenced" and cannot be converted to "unused"
even if no assignment or access is made to them, because they must be present
in the symbol table in order for the program to list. Such variables must
be found by looking at the XREF for variables with reference lists which
contain only defining occurrences (<-DEF
).
One of the most powerful debugging tools available is the capability of single-stepping a program, one line at a time. This process allows the programmer to examine the values of the variables and the sequence in which the program is running at each statement. This is done with the [STEP] key ([f1] in the System menu of an ITF keyboard).
There are three ways to use the [STEP] key:
Type in the following example and execute it by pressing the [STEP] key repeatedly.
10 DIM A(1:5)
20 ! This is an example
30 S=0
40 FOR I=1 TO 5
50 INPUT "Enter a number",A(I)
60 S=S+A(I)
70 NEXT I
80 PRINT S
90 PRINT A(*);
100 END
Notice that the [STEP] key caused every statement to appear in the system message line, one at a time, even those statements that are not really executed, like DIM and comments.
If you are stepping a program and encounter an INPUT, LINPUT, or ENTER KBD statement, you can use [Return], [ENTER], or [CONTINUE] to enter your responses. The system will remember that you are stepping the program and remain in single-step mode after the input operation is complete (unless you press [CONTINUE] again after the input operation is complete).
If you hold down the [STEP] key, to continuously step through program lines, you may want to turn softkey labels off (especially when using bit-mapped alpha displays).
The process of single-stepping, wonderful though it is, can be quite slow, especially if the programmer has little or no idea which part of his program is causing the bug. An alternative way of examining variable changes and program flow is available in the form of the TRACE ALL statement.
When the TRACE ALL command is executed, it causes the system to issue a message prior to executing every line (this shows the order in which the statements were executed), and if the statement caused any variables to change value, a message telling the variables involved and their new values is also issued. The messages are issued to the system message line, and the most useful way to use the TRACE ALL feature is to turn Print All On with the [PRT ALL] key ([f4] in the System menu of an ITF keyboard), unless of course you're a very fast reader. (The printall mode will cause all information from the DISP line, the keyboard input line, and the system message line to be logged on the PRINTALL IS device.)
Turn Print All ON and key in the following example to see how TRACE ALL works:
10 TRACE ALL
20 FOR I=1 TO 10
30 PRINT I;
40 IF I MOD 2 THEN
50 PRINT " is odd."
60 ELSE
70 PRINT " is even."
80 END IF
90 NEXT I
100 END
There are two optional parameters that can be used with TRACE ALL. Both parameters are line identifiers (line numbers or line labels). The first parameter tells the system when to start tracing, and the second one (if it's specified) tells the system when to stop tracing. The following example illustrates the use of one optional line specifier:
1 TRACE ALL 40
10 DIM A(1:10)
20 FOR I=1 TO 100
30 NEXT I
40 FOR J=1 TO 10
50 A(J)=J
60 NEXT J
70 END
It is usually more useful to use the TRACE ALL command from the keyboard
rather than from the program because a program modification is not necessary
if you want to trace a different part of the program. All that's necessary
is to type in a new TRACE ALL command from the keyboard to override the old
one. In the above example, to trace the loop from 20 to 30 instead of the
one from 40 to 60, simply delete line 1 and type in TRACE ALL
20,40
from the keyboard.
10 DIM A(1:10)
20 FOR I=1 TO 100
30 NEXT I
40 FOR J=1 TO 10
50 A(J)=J
60 NEXT J
70 END
The program will begin tracing at line 20, and keep on tracing until it's ready to execute line 40, at which time it will terminate the trace messages and will continue executing the program normally.
If the TRACE ALL statement uses a line label instead of a line number, be aware of what happens if you have more than one occurrence of a given line label in your program. For instance, it is perfectly legal to have the same line label in two or more different program environments--line labels are local to subprograms and branching operations addressing a given line label are treated separately in different subprograms.
However, when a TRACE ALL using a line label is executed, the first line
label in memory is the one that gets used, regardless of the environment
the program was in when the TRACE ALL statement was executed. Thus in the
following program, even though the TRACE ALL Printout
statement is executed inside the subprogram, tracing does not commence until
the subprogram has been exited and the Printout
statement
in the main program has been executed.
10 DIM A(1:10)
20 FOR I=1 TO 10
30 CALL Dummy(A(*),I)
40 GOSUB Printout
50 NEXT I
60 STOP
70 Printout: !
80 FOR J=1 TO 10
90 PRINT A(J);",";
100 NEXT J
105 PRINT
110 RETURN
120 END
130 SUB Dummy(X(*),Z)
140 TRACE ALL Printout
150 FOR I=1 TO 10
160 X(I)=Z*100+I
170 NEXT I
180 GOSUB Printout
190 SUBEXIT
200 Printout: !
210 PRINT "Dummy routine executed";Z
220 RETURN
230 SUBEND
If two line identifiers are used, their location with respect to each other does not matter. Tracing will start when the line specified first is encountered, and it will stop when (or if) the second line is encountered.
The PRINTALL IS command is useful for switching the tracing messages between the CRT and a hardcopy printer. For instance, turning PRINTALL ON during pre-run will allow you to see which array variable has not been dimensioned. (Again, to get any record at all of the trace messages, Print All must be On.) To cause the trace messages to be logged on the CRT, execute PRINTALL IS CRT. (The CRT is the default PRINTALL IS device that the system assumes when it wakes up.) To cause the messages to be logged on a printer, merely change the select code to the appropriate value (PRINTALL IS 701).
The TRACE PAUSE command can be used to set a "break point" in the program. The program will execute at a reduced speed until the specified line is reached, at which time the program will pause, and the specified line will be shown in the display line, indicating that the program will execute it when execution is resumed. Execution may be resumed with the [CONTINUE] key ([f2] in the System and User menus on an ITF keyboard), the [STEP] key ([f1] in the System menu on an ITF keyboard), or by executing CONT from the keyboard (the specified line identifier must be located in the current environment).
By executing the command TRACE PAUSE Printout
from the
keyboard, the following program will pause every time it reaches line 70.
10 DIM A(1:10)
20 FOR I=1 TO 10
40 GOSUB Printout
50 NEXT I
60 STOP
70 Printout: !
80 FOR J=1 TO 10
90 PRINT A(J);",";
100 NEXT J
110 PRINT
120 RETURN
130 END
Try the following ways of continuing execution:
CONT 110
As with TRACE ALL, a new TRACE PAUSE statement overrides a previous one. The same rules are applied when a line label is used in a TRACE PAUSE statement as are applied to the TRACE ALL statement--the first line in memory having that label is used.
TRACE OFF cancels the effects of any active TRACE ALL or TRACE PAUSE statements. The status of Print All and the PRINTALL IS device will be unchanged.
TRACE OFF may be executed either from the program, or from the keyboard.
The [CLR I/O] key ([Break] on the ITF keyboard) suspends any active I/O operation and pauses the program in such a way that the suspended statement will restart once [CONTINUE] ([f2] on the ITF keyboard) or [STEP] ([f1] on the ITF keyboard) is pressed. This is useful for operations which appear to "hang" the machine, such as printing to a printer which isn't turned on.
Most devices will not respond to ENTER requests unless they have first been instructed to respond. If improper values are sent to a device, it may refuse to respond. Therefore, [CLR I/O] can help in debugging these situations.
Here are the operations that can be suspended with [CLR I/O].
SEND | ASSIGN | |
LIST | PRINTALL outputs | PURGE |
CAT | ENTER | CREATE |
OUTPUT | INPUT | |
DUMP GRAPHICS | HP-IB commands | |
DUMP ALPHA | External plotter commands |