Coroutines in Fleet Forth, an ITC Forth.
I discussed coroutines here. This is a more thorough and, I hope, more complete presentation of coroutines in Fleet Forth.
Coroutines are two routines which take turns executing. This is not the same as mutual recursion. Two (or more) routines which are mutually recursive execute one another from the beginning, causing the return stack to grow until the condition is met to not recurse. One place I've seen mutual recursion in Forth is turtle graphics. Mutual recursion in Forth can be achieved with deferred words.
Code:
DEFER LBRANCH
: RBRANCH \ draw right branch and sub-branches of tree
... RECURSE ... ;
... LBRANCH ... ;
: (LBRANCH) \ draw left branch and sub-branches of tree
... RECURSE ... ;
... RBRANCH ... ;
(LBRANCH) IS LBRANCH
In Fleet Forth, the word RECURSE causes the CFA of the word being defined to be compiled.
Note that comments to the end of line in Fleet Forth use the word // since the Commodore 64 lacks a backslash character. The use of the backslash indicates comments which were added to the listing or session log, although in this case, this isn't an actual listing as the ellipses indicate code not shown.
As coroutines switch back and forth, each coroutine resumes where it left off. If two coroutines are infinite loops, they behave like a primitive multitasking system with two tasks that share stacks and run forever. I have yet to think of a good use for such coroutines where a cooperative round robin multitasker wouldn't work better. If anyone knows of such a case, I would like to know about it. Coroutines which are not infinite loops are, in my opinion, far more interesting.
Fleet Forth's coroutine word is CO . CO can be written as a primitive (a code word) or in high level Forth; either way, it swaps the top value on the return stack with the contents of IP , Forth's interpretive pointer. When a high level Forth word executes, do-colon pushes the current contents of IP onto the return stack and places the address of the body of the high level word being executed in IP. EXIT , the word used to return from a high level Forth word to the word which executed it, pulls the top address off the return stack and stores it in IP. When CO is defined as a high level Forth word, do-colon and EXIT move the contents of IP to and from the return stack so all that is necessary, in the body of CO , is to swap the top two items on the return stack.
Code:
: CO 2R> SWAP 2>R ;
Here is a brief example.
Code:
: GREETINGS
CR ." GREETINGS!" CO
CR ." HOW DO YOU DO?" CO
CR ." GOODBYE." ;
: HELLO
GREETINGS
CR TAB ." HELLO THERE!" CO
CR TAB ." AS WELL AS I CAN." CO
CR TAB ." SO LONG." ;
Here is an excerpt of a session log showing the execution of the word HELLO
Code:
GREETINGS!
HELLO THERE!
HOW DO YOU DO?
AS WELL AS I CAN.
GOODBYE.
SO LONG.
Notice that HELLO executes GREETINGS and when GREETINGS exits it exits to HELLO . HELLO exits to whatever executed it.
Here is a slightly different version.
Code:
: GREETINGS
CR ." GREETINGS!" CO
CR ." HOW DO YOU DO?" CO
CR ." GOODBYE." CO
CR ." THANK YOU." ;
: HELLO
GREETINGS
CR TAB ." HELLO THERE!" CO
CR TAB ." AS WELL AS I CAN." CO
CR TAB ." BYE AND TAKE CARE." ;
and an excerpt from the session log where HELLO is executed.
Code:
GREETINGS!
HELLO THERE!
HOW DO YOU DO?
AS WELL AS I CAN.
GOODBYE.
BYE AND TAKE CARE.
THANK YOU.
Once again, HELLO executes GREETINGS . This time, HELLO exits to GREETINGS which exits to INTERPRET .
One coroutine might be an infinite loop. here is an example.
Code:
// DECISIONS DECISIONS
: ANSWER
BEGIN
CR TAB ." MAYBE." CO
CR TAB ." YES!" CO
CR TAB ." NO." CO
AGAIN ;
: QUESTION
CR ." WILL THIS WORK?" ANSWER
CR ." ARE YOU SURE?" CO
CR ." REALLY?" CO
CR ." CAN YOU BE SERIOUS?" CO
CR ." FOR THIS PROJECT?" CO
CR ." ARE YOU QUALIFIED?" CO
CR ." KNOCK IT OFF!" CO
R> DROP ;
And the session log.
Code:
WILL THIS WORK?
MAYBE.
ARE YOU SURE?
YES!
REALLY?
NO.
CAN YOU BE SERIOUS?
MAYBE.
FOR THIS PROJECT?
YES!
ARE YOU QUALIFIED?
NO.
KNOCK IT OFF!
MAYBE.
In this case, when the word QUESTION is finished, it must drop from the return stack the address of the other coroutine before QUESTION exits. What happens if it doesn't? When QUESTION exits, it will exit into ANSWER where ANSWER left off. ANSWER will, on the next execution of CO switch with INTERPRET . When interpret exits, it will exit into ANSWER . When ANSWER switches again, it will be with QUIT . QUIT is an infinite loop which clears the return stack at the start of the loop. That will be the end of the coroutine switching.
Although some words in Fleet Forth can start out as high level then transition to low level and vice versa, the high level portions of mixed level words can use coroutines and even be a coroutine to another word.
Looking back at the second example with GREETINGS and HELLO , the number of coroutine switching in the two words is different. This causes the word which executed the other to be the one which exits to the other. Here is a simplified example.
Code:
// COROUTINE DEMO 1 COROUTINE
: GREETINGS
CR ." GREETINGS!" CO
CR ." AS WELL AS I CAN." ;
: HELLO
GREETINGS
CR TAB ." HI! HOW DO YOU DO?" ;
And the result of executing HELLO .
Code:
GREETINGS!
HI! HOW DO YOU DO?
AS WELL AS I CAN.
Only the word GREETINGS uses the coroutine word, CO . HELLO executes GREETINGS but also exits to GREETINGS. The part of GREETINGS before CO is executed, then GREETINGS switches with HELLO . The part of GREETINGS after CO is executed when HELLO exits. This can be used to advantage. One example is the word RB , the word which causes the word executing it to cause the value of BASE to be restored to what it was prior to the execution of RB .
Code:
: RB ( -- )
BASE @ R> 2>R CO
R> BASE ! ;
The only tricky thing here is to tuck the value of BASE under the return address.
It is possible to craft even more complex words, but care must be exercised. In the following examples, the word (ERR) displays the contents of all of Fleet Forth's stacks.
For each iteration of its loop, the word PROTO will cause execution to pass to the Forth thread which is the body of the word TEST . TEST exits to PROTO which conditionally continues the loop.
Code:
: TEST
CR ." TESTING." (ERR) ;
: PROTO
BEGIN
['] TEST >BODY >R CO
DONE?
UNTIL
(ERR)
CR ." PROTO FINISHED." ;
And here is an excerpt of a session log showing the execution of PROTO .
Code:
TESTING.
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57AF TEST
57C7 PROTO
21A0 INTERPRET
21EB QUIT
TESTING.
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57AF TEST
57C7 PROTO
21A0 INTERPRET
21EB QUIT
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57CF PROTO
21A0 INTERPRET
21EB QUIT
PROTO FINISHED.
This variation of PROTO will cause execution to pass to the Forth thread which is the portion of TEST which occurs after PROTO .
Code:
: PROTO
BEGIN
R@ >R CO
DONE?
UNTIL
(ERR) R> DROP
CR ." PROTO FINISHED." ;
: TEST
PROTO CR ." TESTING." (ERR) ;
Notice this time TEST is executed directly, rather than PROTO . When the loop in PROTO finishes, there is an address of a Forth thread on the return stack. It points into the body of TEST just past where PROTO occurs in the definition of TEST . It must be dropped or that portion of TEST will be executed again. This would not matter for this trivial example, however it will be important later.
Here is an excerpt of a session log where TEST is executed.
Code:
TESTING.
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
5783 TEST
5745 PROTO
5774 TEST
21A0 INTERPRET
21EB QUIT
TESTING.
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
5783 TEST
5745 PROTO
5774 TEST
21A0 INTERPRET
21EB QUIT
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
574D PROTO
5774 TEST
21A0 INTERPRET
21EB QUIT
PROTO FINISHED.
The following version of PROTO has a value which is incremented each time through the loop. A copy is kept on the return stack while another copy is left on the data stack for the thread of Forth from TEST to use.
Code:
: PROTO ( -- N )
0
BEGIN
1+
DUP R@ 2>R CO R>
DONE?
UNTIL
(ERR)
R> DROP
CR ." OUT OF PROTO LOOP"
CR ." LOOPED " . ." TIMES" CR ;
: TEST
0 PROTO + (ERR) ;
And here is an excerpt of a session log where TEST is executed.
Code:
DATA: 1
AUX: EMPTY
RET:
5610 (ERR)
579C TEST
574B PROTO
1
5798 TEST
21A0 INTERPRET
21EB QUIT
DATA: 3
AUX: EMPTY
RET:
5610 (ERR)
579C TEST
574B PROTO
2
5798 TEST
21A0 INTERPRET
21EB QUIT
DATA: 6
AUX: EMPTY
RET:
5610 (ERR)
579C TEST
574B PROTO
3
5798 TEST
21A0 INTERPRET
21EB QUIT
DATA: 6 3
AUX: EMPTY
RET:
5610 (ERR)
5755 PROTO
5798 TEST
21A0 INTERPRET
21EB QUIT
OUT OF PROTO LOOP
LOOPED 3 TIMES
TEST , or any other word which has PROTO , can not use the coroutine word CO after PROTO . If TEST had CO after PROTO , it would be like the other examples with coroutines in that TEST would switch to PROTO without exiting to it. The thread address would be left on the stack and PROTO would pull it from the return stack and use it as the value which is being incremented. The actual value being incremented would be used as the thread of Forth to switch with PROTO . This would most likely cause a crash when PROTO executes CO , depending on the value.
There is nothing preventing TEST from using CO before PROTO or using another word which uses CO , so long as TEST exits to PROTO rather than switching with it.
Code:
: AFTER-. CO TAB U. (ERR) ;
: TEST2 PROTO AFTER-. ;
Here is an excerpt of a session log where TEST2 is executed.
Code:
1
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57B4 AFTER-.
574B PROTO
1
57C4 TEST2
21A0 INTERPRET
21EB QUIT
2
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57B4 AFTER-.
574B PROTO
2
57C4 TEST2
21A0 INTERPRET
21EB QUIT
3
DATA: EMPTY
AUX: EMPTY
RET:
5610 (ERR)
57B4 AFTER-.
574B PROTO
3
57C4 TEST2
21A0 INTERPRET
21EB QUIT
DATA: 3
AUX: EMPTY
RET:
5610 (ERR)
5755 PROTO
57C4 TEST2
21A0 INTERPRET
21EB QUIT
OUT OF PROTO LOOP
LOOPED 3 TIMES
Here is another example:
Code:
: TEST3
PROTO CR U. HELLO ;
And the session log excerpt where TEST3 is executed.
Code:
1
GREETINGS!
HELLO THERE!
HOW DO YOU DO?
AS WELL AS I CAN.
GOODBYE.
BYE AND TAKE CARE.
THANK YOU.
2
GREETINGS!
HELLO THERE!
HOW DO YOU DO?
AS WELL AS I CAN.
GOODBYE.
BYE AND TAKE CARE.
THANK YOU.
3
GREETINGS!
HELLO THERE!
HOW DO YOU DO?
AS WELL AS I CAN.
GOODBYE.
BYE AND TAKE CARE.
THANK YOU.
DATA: 3
AUX: EMPTY
RET:
5610 (ERR)
5755 PROTO
58AE TEST3
21A0 INTERPRET
21EB QUIT
OUT OF PROTO LOOP
LOOPED 3 TIMES
As these examples show, TEST can have other coroutine words in its body after PROTO as long as TEST does not switch with PROTO , it must exit to PROTO because of the way PROTO works.
A few modifications to PROTO will change it into a simpler version of WITH-WORDS .
1) Use the value of CONTEXT and follow the link fields rather than increment the value.
2) Test for the end of a vocabulary and exit the loop by way of WHILE .
3) convert the copy of the value left on the data stack from the LFA to the NFA before CO is executed.
Code:
: WITH-WORDS ( -- NFA )
CONTEXT @
BEGIN
@ ?DUP
WHILE
DUP R@ 2>R L>NAME CO
R>
REPEAT
R> DROP ;
Here is the prototype for WORDS .
Code:
: WORDS ( -- )
WITH-WORDS ID. SPACE ;
Here is a variation on Albert van der Horst's FOR-WORDS and WORDS which have been modified to work better with Fleet Forth.
Code:
// ALBERT'S FOR-WORDS
CODE CO-EXIT ( R: ADR -- )
PLA PLA NEXT JMP END-CODE
: FOR-WORDS
BEGIN
@ ?DUP
WHILE
DUP CO
REPEAT
CO-EXIT ;
: WORDS
CONTEXT @ FOR-WORDS
BEGIN
L>NAME ID. SPACE CO
AGAIN ;
CO-EXIT ( exit the other coroutine) is just another name for RDROP , but the name CO-EXIT is more descriptive of what's happening in this case.
If both WITH-WORDS and FOR-WORDS used CO-EXIT ( or if both used the phrase R> DROP ), the combined size with with each one's respective version of WORDS would be the same, excluding the difference in name sizes. Actually, Albert's version could be made slightly smaller by reclaiming the two bytes used by EXIT , since that version of WORDS is an infinite loop.
Nevertheless, WITH-WORDS and the version of WORDS which uses it is a cleaner implementation. This version of WORDS is not itself a coroutine word. The only parameters on the data stack for the part of WORDS after WITH-WORDS is the NFA of a word in the context vocabulary. Words can be defined to use WITH-WORDS to:
Count the words in a vocabulary:
Code:
: #WORDS ( -- N )
0 WITH-WORDS DROP 1+ ;
Or find the size of all the headers in a vocabulary:
Code:
: HEADERS ( -- N )
0 WITH-WORDS
C@ $1F AND \ get the size
1+ \ add 1 for count byte
2+ \ add 2 for size of link field
+ ;
The general rule for using WITH-WORDS in a word:
1) The part of a word which is before WITH-WORDS is executed once.
2) The part of a word after WITH-WORDS is executed once for each word in the context vocabulary.
3) The net affect WITH-WORDS has on the data stack is to leave the address of a word's NFA for use by that portion of threaded code which follows WITH-WORDS in a definition.
Here is a sample of the output of the simple version of WORDS presented so far:
Code:
WORDS WITH-WORDS ELAPSED CLEAR.TIME TIME TIME! .TIME SEXTAL BCD>INT (ERR)
.
.
.
CURRENT CONTEXT SPAN STATE >IN BLK FENCE
Here is Fleet Forth's WITH-WORDS and WORDS with all enhancements:
Code:
// WITH-WORDS WORDS
: WITH-WORDS ( -- NFA )
CONTEXT @
AHEAD
BEGIN
DUP R@ 2>R L>NAME CO R>
CS-SWAP THEN
@ DUP 0= DONE? OR
UNTIL
R> 2DROP ;
: WORDS ( -- )
COLS ?CR
WITH-WORDS DUP C@ $1F AND 1+ ?CR
ID. TAB ;
And a sample of the output when COLS , a VALUE , is set to 70.
Code:
WORDS WITH-WORDS ELAPSED CLEAR.TIME TIME
TIME! .TIME SEXTAL BCD>INT (ERR) FILES IN-NAME
.
.
.
SPAN STATE >IN BLK FENCE
Albert van der Horst also introduced the word AFTER-DROP , which drops an item from the data stack after the word in which it is used exits. In the case of a word which uses WITH-WORDS , If AFTER-DROP occurs before WITH-WORDS then an item is dropped from the data stack after the word which uses WITH-WORDS exits.
Code:
: AFTER-DROP
CO DROP ;
Here is a trivial example to demonstrate.
Code:
: WORDS-3DROP
AFTER-DROP AFTER-DROP AFTER-DROP
WITH-WORDS
ID. SPACE ;
This word will display all the words in the context vocabulary AND drop three items from the data stack.
Not a very useful word, I know. Here is one that is:
Code:
: IN-NAME ( ADR -- )
AFTER-DROP
WITH-WORDS
2DUP
COUNT $1F AND >HERE CLIP COUNT
2+ TRUE UNDER+
BL HERE C!
ROT COUNT MATCH NIP
IF CR ID. EXIT THEN
DROP ;
IN-NAME takes the address of a counted string ( the sub string) and displays the name of every word in the context vocabulary which contains that string in its name. The drop in AFTER-DROP drops the address of the sub string at the conclusion of IN-NAME .
Here is an excerpt from a session log where I've added comments.
Code:
" AD" IN-NAME \ typed by me. substring: 'AD'
.ADDR
ADMODE
ADDRESSES
EAD
SAD
ADEPTH
AHEAD
LOAD
LINELOAD
ADD
PAD OK
" AD" IN-NAME \ typed by me. substring: ' AD'
ADMODE
ADDRESSES
ADEPTH
ADD OK
" AD " IN-NAME \ typed by me. substring: 'AD '
EAD
SAD
AHEAD
LOAD
LINELOAD
PAD OK
The lines with the comment "typed by me. substring: 'xxx'" were entered at Forth's command line. Everything else in this excerpt is the computer's response.