Debugging Programs

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.

Using Live Keyboard

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.)

Executing Commands While a Program Is Running

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
 

Using Program Variables

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.

Calling Subprograms

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.

Pausing and Continuing a Program

You can also pause a program from the keyboard using the [PAUSE] ([Break]) key.

You may subsequently continue program execution:

Note that a program which has been edited cannot be continued.

Keyboard Commands Disallowed During Program Execution

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

Cross References

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.

Generating a Cross-Reference Listing

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 
 

Example Program and Cross Reference

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:

Unused Entries

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).

Single-Stepping a Program

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:

  1. If the program is stopped (i.e., a pre-run has to be performed), pressing the [STEP] key will cause the system to perform a pre-run on the program, but no program lines will actually be executed. The first line that will be executed will appear in the system message line at the bottom of the screen. Pressing the [STEP] key again will cause that line to be executed, and the next line after that to be executed will appear in the message line. If the [STEP] key is pressed causing the next line to appear in the display, and a live keyboard operation (such as examining the value of a variable) is performed, the contents of the message line will change. Pressing the [STEP] key again will still cause the line to be executed, even though it is no longer visible in the display line. After the statement has completed, the next line will appear.
  2. If the program is in an INPUT or LINPUT statement, pressing the [STEP] key is sufficient to terminate the operation. Any data entered from the keyboard will be entered into the correct variables, just as though [CONTINUE] ([f2] on the ITF keyboard) or [ENTER] ([Return] on the ITF keyboard) had been pressed, but program execution will be PAUSEd, and the statement immediately following the INPUT or LINPUT will appear in the system message line.
  3. If the program is in a PAUSEd state, pressing the [STEP] key will cause the next line to be executed. The program counter will not be reset, nor will a pre-run be performed. Again, the next line to be executed will appear in the system message line after the last one has been completed. A paused state is indicated by a dash in the run light in the lower right-hand corner of the screen.

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).

Tracing

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.

TRACE ALL

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.

PRINTALL IS

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).

TRACE PAUSE

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:

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

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 (Break) Key

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].
PRINT SEND ASSIGN
LIST PRINTALL outputs PURGE
CAT ENTER CREATE
OUTPUT INPUT
DUMP GRAPHICS HP-IB commands
DUMP ALPHA External plotter commands