Tracing by hand is useful when working out the logic of a yet to be written word. If a word is already defined, a TRACE word is useful to help find out where something is going wrong or verify that a word is working properly. A TRACE word is also useful when a hand trace is impractical, such as when testing coroutines.
I have on more than one occasion mentioned Blazin' Forth's TRACE word. I had a version of TRACE modified to work with Fleet Forth and have enhanced it to some degree. I finally got around to writing Fleet Forth's own TRACE word inspired by Blazin' Forth, but taking advantage of Fleet Forth's >FORTH and >ASSEM . Fleet Forth's TRACE functionality, while having a look and feel similar to Blazin' Forth's TRACE , is implemented differently.
>NEXT and NEXT> are used to patch and restore NEXT .
Blazin' Forth's word >NEXT patches NEXT roughly in the middle. Fleet Forth's >NEXT patches NEXT as close to the beginning as possible. There is a primitive, BRANCH , which jumps to NEXT 2+ to save two cycles because it leaves a one in the Y-register; therefore, the earliest point NEXT can be patched, without re-implementing BRANCH , is two bytes in.
Code:
CODE >NEXT ( ADR -- )
// POINTS NEXT TO ADR
$4C # LDA NEXT 2+ STA
0 ,X LDA NEXT 3 + STA
1 ,X LDA NEXT 4 + STA
POP JMP END-CODE
This leaves the first instruction of NEXT intact and overwrites the next two. The other instructions in NEXT are intact.
NEXT> restores NEXT . It is included in the kernel so FORGET can restore NEXT .
Code:
LABEL NEXT
1 # LDY
LABEL NEXT1
IP )Y LDA W 1+ STA DEY
LABEL NEXT2
IP )Y LDA W STA
1 # LDA
SEC IP ADC IP STA
CS NOT IF
W 1- JMP
THEN
IP 1+ INC
W 1- JMP END-CODE
CODE NEXT> ( -- ) // RESTORE NEXT
2 # LDY
BEGIN
NEXT2 ,Y LDA
NEXT1 ,Y STA DEY
0< UNTIL
NEXT JMP END-CODE
Two variables, <IP and IP> , are used to check if IP is in the address range for a TRACE .
TRACE gets a name from the text stream and uses AFIND to find where that word ends.
AFIND takes an address, any address, and finds the nearest link fields below and above that address. It checks all vocabularies. It's reasonably fast because it uses a primitive to search a vocabulary for the nearest link fields above and below. Fleet Forth already uses this word for some components of SEE .
Code:
: TRACE ( ++ )
NEXT>
' AFIND NIP IP> ! <IP !
STEP >NEXT ;
STEP is a SUBR , a subroutine in Fleet Forth. It returns its PFA just like a VARIABLE . TRACE stores the beginning and end of a word in <IP and IP> respectively and patches NEXT to jump to the body of STEP .
Code:
SUBR STEP
<IP LDA IP CMP
<IP 1+ LDA IP 1+ SBC
CS NOT IF
IP> LDA IP CMP
IP> 1+ LDA IP 1+ SBC
CS NOT ELIF
BEGIN CS-SWAP
IP )Y LDA W 1+ STA
NEXT 6 + JMP
THEN
>FORTH
NEXT> R@ HLD @ >R +PAD
CR .STEP
KEY DUP ASCII" {RUN/STOP}" =
IF DROP NOTRACE THEN
ASCII" {CONTROL-P}" =
IF
CON OFF
BEGIN
CR ." P? " QUERY INTERPRET
STATE @ 0= IF ." OK" THEN
CON @
UNTIL
THEN
-PAD R> HLD ! RECURSE >NEXT
>ASSEM
INY
0= UNTIL END-CODE
The following tests if IP is in the range for a TRACE .
Code:
<IP LDA IP CMP
<IP 1+ LDA IP 1+ SBC
CS NOT IF
IP> LDA IP CMP
IP> 1+ LDA IP 1+ SBC
CS NOT ELIF
BEGIN CS-SWAP just marks the destination for a backward jump and places that information under the control flow data for the IF ELIF THEN structure.
When IP is not in range, NEXT needs to run to completion. Since the Y-register is not altered by the comparison, it still holds a value of one. The following does the work of the overwritten instructions in NEXT and jumps to the rest of NEXT .
Code:
IP )Y LDA W 1+ STA
NEXT 6 + JMP
When IP is in range, the rest of STEP runs.
Code:
>FORTH
NEXT> R@ HLD @ >R +PAD
CR .STEP
KEY DUP ASCII" {RUN/STOP}" =
IF DROP NOTRACE THEN
STEP shifts to high level Forth. NEXT> switches off tracing. The top of the return stack is fetched. This will be the value of IP pushed there by the shift to high level Forth. The current value of HLD is pushed on the return stack and the gap between HERE and PAD is altered.
Code:
: +PAD
$FF ?MEM
[ ' PAD >BODY 4 + ] LITERAL C! ;
This allows tracing pictured numeric output words.
.STEP displays the information such as the name of the word about to be executed and the contents of the data stack, for this step. .STEP is a DEFERred word so it can be set to an appropriate display word for the given trace.
Code:
DEFER .STEP
: (.ST1) ( IP -- )
@ .NAME TAB .S ;
' (.ST1) IS .STEP
: (.ST2) ( IP -- )
(.ST1) CR TAB .AS ;
: (.ST3) ( IP -- )
(.ST2)
2R> .RS 2>R ;
(.ST2) would be useful when tracing a word which uses the auxiliary stack and (.ST3) would be useful when tracing coroutine words since it shows the contents of the return stack.
{RUN/STOP} and {CONTROL-P} are where the RUN/STOP and CONTROL-P keys are used on the Commodore 64. They do not display properly in a print dump.
If the RUN/STOP key is pressed, tracing stops. STEP calls NOTRACE which restores NEXT , restores the gap between HERE and PAD then QUITs. Before NOTRACE runs, NEXT is already restored, but NOTRACE can also be used at the command line.
Code:
: -PAD
$55
BRANCH [ ' +PAD >BODY 5 + , ] -;
: NOTRACE
NEXT> -PAD
." TRACING OFF" QUIT -;
If the Control-P key combination is pressed, a mini quit loop runs. This is useful for checking other data or changing some of the items on the return stack.
Code:
ASCII" {CONTROL-P}" =
IF
CON OFF
BEGIN
CR ." P? " QUERY INTERPRET
STATE @ 0= IF ." OK" THEN
CON @
UNTIL
THEN
The mini quit loop runs until the word CONT is entered.
Code:
VARIABLE CON
: CONT CON ON ;
If a word being traced is about to branch out to another word, the trace can be changed to that word.
An example is FORGET . It branches into IORESET . When the trace shows that the branch to IORESET is to be executed, I can press Control-P. While in the mini quit loop I can type the following to resume tracing where FORGET branches.
Code:
TRACE IORESET
CONT
Code:
-PAD R> HLD ! RECURSE >NEXT
>ASSEM
INY
0= UNTIL END-CODE
STEP restores the gap between HERE and PAD and restores the previous value of HLD .
Although RECURSE is used, STEP is not recursive. RECURSE compiles STEP which places its PFA on the data stack. This value is used to patch NEXT so tracing can continue. >ASSEM shifts the word to low level code. In the process of shifting to low level >ASSEM performs the function of EXIT by pulling the top value from the return stack and storing it in IP . At this point the Y-register has a value of zero. It is incremented followed by a jump to the address marked by BEGIN .
Code:
BEGIN CS-SWAP
IP )Y LDA W 1+ STA
NEXT 6 + JMP
There is another tool I like from Blazin' Forth, WATCH . WATCH watches two consecutive memory locations (16 bits) such as a VARIABLE , VALUE or other location which is getting changed unexpectedly. Here is Fleet Forth's implementation.
Code:
: WATCH ( ADR -- )
DUP @ EYE !
// PATCH LOOK
DUP LOOK 1+ !
1+ LOOK 9 + !
LOOK >NEXT ;
WATCH stores the original 16 bit value from the address on the stack in the variable EYE . It then patches the SUBRoutine LOOK with the address.
Code:
VARIABLE EYE
SUBR LOOK
TRUE LDA EYE CMP
0= IF
TRUE LDA EYE 1+ CMP
0= IF
IP )Y LDA W 1+ STA
NEXT 6 + JMP
THEN
THEN
INY
BEGIN
NEXT 7 + ,Y LDA
NEXT 2+ ,Y STA DEY
0< UNTIL
>FORTH
.RS TRUE
ABORT" WATCHED MEMORY ALTERED" -;
The following tests if the memory locations have been altered.
Code:
TRUE LDA EYE CMP
0= IF
TRUE LDA EYE 1+ CMP
0= IF
At this point the Y-register has a value of one. If the memory under observation has not been changed, go back to NEXT the same as STEP
Code:
IP )Y LDA W 1+ STA
NEXT 6 + JMP
If the memory under observation has been altered, NEXT must be restored before shifting to high level to avoid "infinite recursion."
Code:
INY
BEGIN
NEXT 7 + ,Y LDA
NEXT 2+ ,Y STA DEY
0< UNTIL
With NEXT restored, LOOK shifts to high level. The high level section displays the return stack with .RS and aborts with the message "WATCHED MEMORY ALTERED."
The session log of the test.
Code:
OK
VARIABLE NONESUCH OK
$1234 NONESUCH ! OK
NONESUCH WATCH OK
: TEST
1 NONESUCH +! ; OK
: TEST2 TEST ; OK
: TEST3 TEST2 ; OK
TEST3
22464 LOOK
23515 TEST
23531 TEST2
23547 TEST3
8557 (I/C)
8629 INTERPRET
8724 QUIT
TEST3
^^^^^
WATCHED MEMORY ALTERED