Classic Computer Magazine Archive ANTIC VOL. 2, NO. 1 / APRIL 1983

Trace Utility

by Alan Filipski

Alan Filipski responded to our challenge to develop a trace routine (see ANTIC #4, page 6) and is the winner of that contest. His prize is a copy of Basic A+, by Optimized Systems Software. His program and explanatory article is published for the benefit of all of us.

When debugging a BASIC program it is often very valuable to have some way to trace the order of execution of statements within the program. This is frequently done by inserting "debug" print statements within the program and later deleting them. We could also sit down with the program listing and "play computer", simulating execution of the program. Neither of these ways is completely satisfactory. It is a nuisance to insert print statements for debugging and later have to delete them. We all know that when trying to follow a program listing, we can be completely blind to an obvious error because we see a statement as it "should be" rather than as it actually is.

The BASIC Trace Utility program given here is intended to be an aid in situations like this. It monitors the execution of your program, displaying each line as it is executed, and can display values of variables when requested. At any time, you may halt the execution, modify or display other variables, and then resume execution where you left off. This utility may be used to determine where a variable takes on an erroneous value or at what point the program takes an execution path contrary to the programmer's intention. In addition, the beginning programmer can use the trace utility to better understand what happens when a BASIC program executes.

The BASIC Trace Utility is written in BASIC and may be run on any ATARI 400 or 800 system with the BASIC cartridge and at least 16K RAM. The principal limitation on the user program is that it may not use line numbers greater than 30999, since this upper range of line numbers is used for the trace utility program.

To use the program, it must first be loaded with the program you want to debug. In order to do this conveniently, the trace utility program should have been stored on disk in source form using the command "LIST D1: TRACE.LST" (rather than the SAVE command) and should be merged with your loaded program using the command "ENTER D1:TRACE.LST" (rather than the LOAD command ).

To start the trace, type "GOTO 31000". The trace utility program will then ask you for the following information: the line number at which to begin tracing, and the number of lines to trace. The trace utility will then begin to execute your program. As each line of your program is executed that line will be printed out. Any output from your program will be printed out interspersed with this trace listing. This will continue until the number of lines you have requested have been traced. (DATA statements, not being executable, are neither printed out nor counted.) At that point, the trace utility will ask you how many more lines you wish to trace.

After tracing these lines, the program will again ask you how many more lines you wish to trace. This cycle will continue until either your program ends or you enter a "0" in response to the query. At this point, control is returned to the immediate mode. If you wish, you may now print out or modify variables, GOTO 31000 and restart the trace, either at the beginning, or at the line where your previous trace left off. If you start at the beginning, all variables will be cleared, and arrays and strings will be deallocated.

If there are any variables which you want printed out automatically at every step of the trace, you may insert your own PRINT statements anywhere in the line-number range 31122-31126. These PRINT statements will not be traced, but will be executed before each line of the user program is executed. To produce a more compact display, end each PRINT statement with a semicolon.

There are a few cautions and limitations to be observed when using this program:

The user program should not contain any TRAP or CLR statements.

If the user program terminates by executing a STOP or END which is not the first statement of the line in which it appears, for example:

910 PRINT "NORMAL TERMINATION":END or

910 INPUT A:IF A=0 THEN STOP

then the user program will be seen to contain some garbage when it is listed. If this happens, re-enter the trace utility by typing GOTO 31000, and exit by requesting 0 lines to be traced. This minor nuisance cannot be reasonably repaired within the framework of the existing design of the trace utility.

It is wise to maintain a backup copy on disk of any program being traced (or just being run).

The trace utility program uses some BASIC variable names, all of which begin with "DBG". Avoid using variable names in your program which start with this sequence of characters.

As mentioned before, the program being traced should not use any line numbers greater than 30999.

Do not expect the traced program to run as fast as the original program.

How does the BASIC Trace Utility program work? Since BASIC is an interpreted language, the simplest and most straightforward way to produce a trace utility would be to modify the interpreter. In the case of ATARI BASIC however, this alternative is not available, since the interpreter is in a ROM cartridge. The approach taken here is more similar to the approach that might be taken to trace a compiled language and involves setting trappable errors in each line of the user program, and then listing the line when the error trap is taken. This is admittedly a kludge, but I could not think of a better way to do it.

This is what happens when the user types in "GOTO 31000": The program first sets a TRAP so that any execution error causes control to go to statement 31046. Then the first command token in each line of the user program (except DATA statements) is set to 55, meaning "syntax error" to the ATARI BASIC interpreter. The real command token is stored by adding it into the "end-of-line" token for that line so that it may be retrieved later. The program then transfers control to the line number input by the user. Since the first statement of this line contains an error, control passes to 31046. This portion of the Trace Utility program re-introduces any errors which were cleared on a previous cycle, clears the error in the line which caused the trap, and LISTs this line. The line containing the most recently executed FOR or GOSUB statement, if any, is also cleared of its error. This is necessary, because whenever the interpreter encounters a NEXT or RETURN, it checks to see whether the corresponding FOR or GOSUB is still there. Control is now transferred to the (now corrected) statement which caused the error trap and the statement is executed. When control passes from this line to any other line, however, an error trap is taken and the cycle repeats. When the user indicates that he is done by entering a "0", errors are removed from all lines and the program stops.

The fact that this program was written in BASIC has several advantages. First, it is compact, consisting of less than 90 lines of executable code with only 12 variables. The primary advantage, however, is that it may be readily modified by the user. For example, if it were desired to print only the line number of the statement being traced and not the entire statement, it is only necessary to change line 31130 to

31130 ? PEEK(DBGPTR)+256*PEEK(DBGPTR+1);" ";

This ability to easily modify the source gives the user quite a bit of flexibility once he understands the code. Understanding the source code may take some work because of the high density of PEEK$ and POKE$ and the lack of such niceties as WHILE loops and indentation in ATARI BASIC. To aid understanding, here is a description of variables used in the program:

DBGCOM-Variable used to store the command token of a BASIC statement.

DBGEOL-Variable used to store the end-of-line token of a BASIC statement.

DBGLN1/DBGLN2-used to hold two-byte line number of a statement which triggered trap.

DBGPTR -Pointer to beginning of current statement; used in a loop to search for a particular line number.

DBGSAVE - A temporary holder for the value of DBGPTR.

DBGSAV1 - Statement-table offset of most recently executed FOR or GOSUB statement.

DBGSAV2 - Statement-table offset of statement most recently cleared for execution.

DBGST-Address of beginning of BASIC statement table.

DBGSTART-Line number at which trace execution is to start .

DBGTC-Count of number of lines left to trace.

DBGTOP-Address of top of BASIC run-time stack.

A further explanation of these concepts may be found in the book De Re ATARI.

Writing this program was very instructive and required some experimentation to discover undocumented details of the BASIC interpreter. Given the limitations described above, it provides a useful utility for debugging programs written in ATARI BASIC.

RAM REQUIREMENTS
TRACE 3K + traced program

Listing: TRACE.BAS Download
Listing: TRACE.LST Download / View
31000 TRAP 31046
31002 REM 
31004 REM BASIC TRACE UTILITY
31006 REM ALAN FILIPSKI 1982
31008 REM 
31010 REM FIND START OF STMNT TABLE
31012 DBGST=PEEK(136)+256*PEEK(137)
31014 REM SET ERR IN EACH STMNT
31016 GOSUB 31152
31018 ? " ":? "BASIC TRACE UTILITY":? " "
31020 ? "ENTER LINE NUMBER AT WHICH"
31022 ? "EXECUTION IS TO START"
31024 ? "(FIRST LINE IN PROG IS ";PEEK(DBGST)+256*PEEK(DBGST+1);")"
31026 INPUT DBGSTART
31028 REM CLR ARRAYS IF STARTING AT FIRST STATEMENT
31030 IF DBGSTART<>PEEK(DBGST)+256*PEEK(DBGST+1) THEN 31038
31032 CLR :DBGST=PEEK(136)+256*PEEK(137)
31034 REM RESTORE DBGSTART
31036 DBGSTART=PEEK(DBGST)+256*PEEK(DBGST+1)
31038 ? "HOW MANY LINES TO TRACE ":INPUT DBGTC
31040 IF DBGTC<=0 THEN 31064
31042 GOTO DBGSTART
31044 REM TRAP HERE AT EACH ERROR ENCOUNTERED
31046 DBGST=PEEK(136)+256*PEEK(137)
31048 IF PEEK(195)=17 THEN 31054
31050 REM UNEXPECTED ERROR TYPE
31052 ? "ERR TYPE ";PEEK(195);" AT LINE ";PEEK(186)+256*PEEK(187):GOTO 31064
31054 IF DBGTC>0 THEN 31072
31056 ? "HOW MANY MORE ";
31058 INPUT DBGTC
31060 IF DBGTC>0 THEN 31072
31062 REM TIME TO QUIT. REMOVE ERRS
31064 GOSUB 31212
31066 ? "TRACE ABORTED"
31068 STOP 
31070 REM REINTRODUCE ERR INTO CLEARED "FOR" OR "GOSUB"
31072 IF DBGSAV1=0 THEN 31086
31074 DBGSAV1=DBGSAV1+DBGST:IF PEEK(DBGSAV1+4)=55 THEN 31086
31076 DBGEOL=DBGSAV1+PEEK(DBGSAV1+2)-1
31078 DBGCOM=DBGSAV1+4
31080 POKE DBGEOL,PEEK(DBGEOL)+PEEK(DBGCOM)
31082 POKE DBGCOM,55
31084 REM REINTRODUCE ERR INTO LAST STATEMENT EXECUTED
31086 DBGSAV2=DBGSAV2+DBGST:IF PEEK(DBGSAV2+4)=55 THEN 31098
31088 DBGEOL=DBGSAV2+PEEK(DBGSAV2+2)-1
31090 DBGCOM=DBGSAV2+4
31092 POKE DBGEOL,PEEK(DBGEOL)+PEEK(DBGCOM)
31094 POKE DBGCOM,55
31096 REM CLEAR ERR FROM "FOR" OR "GOSUB" ON TOP OF RUNSTK
31098 DBGSAV1=0
31100 IF PEEK(142)=PEEK(144) AND PEEK(143)=PEEK(145) THEN 31114
31102 DBGTOP=PEEK(144)+256*PEEK(145)
31104 DBGLN1=PEEK(DBGTOP-3)
31106 DBGLN2=PEEK(DBGTOP-2)
31108 GOSUB 31176
31110 DBGSAV1=DBGPTR-DBGST
31112 REM FIND STATEMENT WHICH TRIGGERED TRAP AND CLEAR ERR
31114 DBGLN1=PEEK(186)
31116 DBGLN2=PEEK(187)
31118 GOSUB 31176
31120 DBGSAV2=DBGPTR-DBGST
31122 REM ***************************
31124 REM INSERT USER PRINT STATEMENTS HERE
31126 REM ***************************
31128 REM LIST TRAPPED STMNT
31130 LIST PEEK(DBGPTR)+256*PEEK(DBGPTR+1)
31132 TRAP 40000:TRAP 31046
31134 DBGTC=DBGTC-1
31136 REM IF STMNT IS END OR STOP, CLEAR ALL STATEMENTS
31138 IF PEEK(DBGPTR+4)=21 OR PEEK(DBGPTR+4)=38 THEN GOSUB 31212
31140 REM EXECUTE STATEMENT
31142 GO TO PEEK(DBGPTR)+PEEK(DBGPTR+1)*256
31144 REM 
31146 REM SUB TO SET ERRS
31148 REM SET COMMAND TOKEN IN ALL USER STATEMENTS (EXCEPT DATA STATEMENTS) TO 55 (=ERROR)
31150 REM SAVE ORIGINAL USER COMMAND BY ADDING TO END-OF-LINE BYTE
31152 DBGPTR=DBGST
31154 IF PEEK(DBGPTR+4)=55 OR PEEK(DBGPTR+4)=1 THEN 31164
31156 DBGEOL=DBGPTR+PEEK(DBGPTR+2)-1
31158 DBGCOM=DBGPTR+4
31160 POKE DBGEOL,PEEK(DBGEOL)+PEEK(DBGCOM)
31162 POKE DBGCOM,55
31164 DBGPTR=DBGPTR+PEEK(DBGPTR+2)
31166 IF PEEK(DBGPTR)+256*PEEK(DBGPTR+1)<31000 THEN 31154
31168 RETURN 
31170 REM 
31172 REM SUBROUTINE TO FIND STMNT AND REMOVE ERR.  2-BYTE LINE NO. EXPECTED IN DBGLN1 AND DBGLN2.
31174 REM PTR TO LINE IS RETURNED IN DBGPTR.
31176 DBGPTR=DBGST
31178 IF DBGLN1=PEEK(DBGPTR) AND DBGLN2=PEEK(DBGPTR+1) THEN 31186
31180 DBGPTR=DBGPTR+PEEK(DBGPTR+2)
31182 GOTO 31178
31184 REM REMOVE ERR
31186 IF PEEK(DBGPTR+4)<>55 THEN RETURN 
31188 DBGEOL=DBGPTR+PEEK(DBGPTR+2)-1
31190 DBGCOM=DBGPTR+4
31192 IF PEEK(DBGEOL)>100 THEN 31200
31194 POKE DBGCOM,PEEK(DBGEOL)-22
31196 POKE DBGEOL,22
31198 RETURN 
31200 POKE DBGCOM,PEEK(DBGEOL)-155
31202 POKE DBGEOL,155
31204 RETURN 
31206 REM 
31208 REM SUBROUTINE TO CLEAR ALL ERRORS
31210 REM 
31212 DBGSAVE=DBGPTR:DBGPTR=DBGST
31214 IF PEEK(DBGPTR+4)<>55 THEN 31228
31216 DBGEOL=DBGPTR+PEEK(DBGPTR+2)-1:DBGCOM=DBGPTR+4
31218 IF PEEK(DBGEOL)>100 THEN 31226
31220 REM NON-REM LINE HAS 22 FOR EOL
31222 POKE DBGCOM,PEEK(DBGEOL)-22:POKE DBGEOL,22:GOTO 31228
31224 REM REM LINE HAS 155 FOR EOL
31226 POKE DBGCOM,PEEK(DBGEOL)-155:POKE DBGEOL,155
31228 DBGPTR=DBGPTR+PEEK(DBGPTR+2)
31230 IF PEEK(DBGPTR)+256*PEEK(DBGPTR+1)<31000 THEN 31214
31232 DBGPTR=DBGSAVE
31234 RETURN