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.
TRACE 3K + traced program
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