6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Sat Sep 21, 2024 5:46 pm

All times are UTC




Post new topic Reply to topic  [ 354 posts ]  Go to page Previous  1 ... 10, 11, 12, 13, 14, 15, 16 ... 24  Next
Author Message
PostPosted: Sat Apr 17, 2021 5:55 am 
Offline

Joined: Sun Apr 26, 2020 3:08 am
Posts: 357
GARTHWILSON wrote:
I guess it would be a little strange to have screens numbered in something other than decimal.

I find it stranger to have screens numbered in decimal. But that's just because I wrote some words based on screens being multiples of 16 (0-$F). Also, being on a hard drive and not dealing with floppies really helps. Screen files can be created to be multiples of $50 (80), $100 (256), $200 (512), up to $8000 (32768) screens at a time, which is the Prodos max file size on the Apple II.

Personally, I just find it easier to count in hexadecimal.


Top
 Profile  
Reply with quote  
PostPosted: Thu Apr 22, 2021 2:32 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

Whether listing in hexadecimal, decimal, or some other base, the base used to list is independent of the base used for loading.
The advantages of my modifications, for me, are:
1) I don't have to specify the base in the source of a screen. If I need a number in hexadecimal, I can prefix the number with the character $.
2) If I need to load some tools, whatever base I'm working in is automatically restored after the load.


Top
 Profile  
Reply with quote  
PostPosted: Thu Apr 22, 2021 2:58 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890
barrym95838 wrote:
I'm not properly equipped to give any meaningful critiques, but I'm very happy to tag along and try to learn a thing or two.


You don't need to let that stop you. Although I eventually went with AYPUSH (which has the high byte in Y) as well as keeping PUSH , your suggestion about PUSHAY got me thinking in that direction.


Top
 Profile  
Reply with quote  
PostPosted: Tue Apr 27, 2021 12:16 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

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.


Top
 Profile  
Reply with quote  
PostPosted: Thu Jun 03, 2021 2:40 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've recently removed the virtual memory words from Fleet Forth's metacompiler's source, which it uses to build the target kernel, and placed them in the system loader but may move them to the optional utilities.
Since the kernel is less than 10k, Fleet Forth's virtual memory words are limited to 64k. Since a Ram Expansion Unit with 256 banks, the maximum possible with a C64 REU, can have 16 megabytes, I mention some ways around this limitation later in this post.
Fleet Forth uses blocks in the REU for virtual memory. Blocks in the REU start at block number 32768, or $8000. Fleet Forth includes a convenience word, RAM , which takes a number from the stack and adds $8000.
SPLIT , defined in the kernel, splits a number into its low and high bytes with the high byte on top.
JOIN takes a low byte and high byte and joins them into a sixteen byte number.
U/BBUF ( U divided by B/BUF ) divides an address by B/BUF and returns an offset into a block and a block number.
>VIRTUAL takes a virtual address and returns an address in a block buffer.
VC@ and VC! fetch and store a byte respectively in virtual memory.
V@ and V! are the virtual memory counterparts of @ and ! built on VC@ and VC! .
VDUMP takes an address and count. It does the same thing as DUMP , dump the contents of memory to the screen, or other output device, but it dumps the contents of virtual memory.
VMSAVE ( virtual memory save ) saves a section of virtual memory to a file on a Commodore 64 disk drive. VMSAVE takes the starting address, the address just past the end of the section of memory to be saved, the address of a name, and the length of the name. The name must end with ",P,W" to write the section of memory as a Commodore 64 program file.
There are a few display formatting words. 8BITS and 16BITS return the widths of the string representation of the largest 8 bit number and 16 bit number respectively. SETWIDTH sets these words according to the current number base.
Code:
CODE JOIN  ( LO HI -- U )
   0 ,X  LDA  3 ,X STA
   POP JMP  END-CODE
CODE U/BBUF  ( U -- OFFSET BLK# )
   1 ,X LDA  TAY
   3 # AND  1 ,X STA
   TYA  .A LSR  .A LSR
   APUSH JMP  END-CODE
: >VIRTUAL  ( VADR -- ADR )
   U/BBUF RAM BLOCK + ;
: VC@  ( VADR -- B )
   >VIRTUAL C@ ;
: VC!  ( B VADR -- )
   >VIRTUAL C! UPDATE ;
: V@  ( VADR -- N )
   DUP VC@  SWAP 1+ VC@  JOIN ;
: V!  ( N VADR -- )
   SWAP SPLIT
   ROT TUCK 1+ VC! VC! ;
: CMOVE>V  ( ADR VADR CNT -- )
   BOUNDS
   ?DO  COUNT I VC!  LOOP
   DROP ;
: (VDUMP)  ( ADR -- )  8 BOUNDS
   DO  I VC@ 8BITS .R SPACE  LOOP ;
: VDUMP  ( ADR CNT -- )
   SETWIDTH BOUNDS
   DO
      CR I DUP 16BITS U.R 2 SPACES
      DUP (VDUMP) SPACE
      DUP 8 + (VDUMP)
      2 SPACES 16 BOUNDS
      DO
         I VC@ QEMIT
      LOOP
      DONE? ?LEAVE
   16 +LOOP
   CR ;

The code for VMSAVE is somewhat convoluted just prior to and within the DO LOOP . When writing virtual memory to disk, I was trying to avoid opening and closing a channel for each byte written and it got a little messy. I plan to test a new version of VMSAVE to see how opening and closing the channel for each byte affects the time to save memory.
Code:
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   1 CHKOUT IOERR
   SWAP DUP SPLIT SWAP
   (EMIT) (EMIT) CLRCHN
   DUP VC@ DROP 1 CHKOUT IOERR
   ?DO  I B/BUF MOD 0=
      IF  CLRCHN  THEN
      I VC@  I B/BUF MOD 0=
      IF  1 CHKOUT IOERR  THEN
      (EMIT)
   LOOP
   CLRCHN  1 CLOSE ;

These are the only general purpose virtual memory words needed by the metacompiler.
For completeness, here is CMOVEV> to move memory from virtual memory to the C64's memory.
Code:
: CMOVEV>  ( VADR ADR CNT -- )
   0
   DO
      OVER I + VC@  OVER I + C!
   LOOP
   2DROP ;

The metacompiler adds a few more virtual memory words specifically related to building a system in virtual memory. Here are just a few.
TDP target dictionary pointer.
THERE target HERE .
VALLOT virtual ALLOT .
VC, and V, the virtual memory counterparts of C, and , .
SALLOT string (at HERE ) allot. Takes size n on the stack and copies a string from HERE to THERE and allots n bytes of virtual memory.

Three possibilities come to mind to store more data than 64k in virtual memory.
64k at a time could be stored and the block moving words could be used to move those 64 blocks to higher numbered blocks in virtual memory. This is feasible because Fleet Forth has the word BMOVE to move blocks. The first 64 blocks in the REU could be copied to the next 64 with the following:
Code:
0 RAM 64 RAM 64 BMOVE

Another approach is modifying >VIRTUAL .
Code:
0 VALUE BANK
: >VIRTUAL  ( VADR -- ADR )
   U/BBUF RAM BANK 6 LSHIFT + BLOCK + ;

The REU can be though of as having a certain number of 64k banks (which is actually the case). The first 64k blocks can be selected for virtual memory with:
Code:
0 TO BANK

the next 64k blocks with:
Code:
1 TO BANK

and so on.

The third approach is to define a set of long virtual memory access words.
Code:
: >LVIRTUAL  ( LADDR -- ADDR )
   B/BUF UM/MOD RAM BLOCK + ;
: VLC@  ( LADDR -- B )
   >LVIRTUAL C@ ;
: VLC!  ( B LADDR -- )
   >LVIRTUAL C!  UPDATE ;
: VL@  ( LADDR -- N )
   2DUP VLC@  -ROT 1 0 D+
   VLC@ JOIN ;
: VL!  ( N LADDR -- )
   ROT SPLIT  2OVER 1 0 D+ VLC!
   -ROT VLC! ;

The LADDR in the stack comments is a double number virtual memory address. Here are the rest:
Code:
: VLCOUNT  ( LADDR -- LADDR+1 B )
   2DUP VLC@ -ROT 1 0 D+ ROT ;
: (VLDUMP)  ( LADDR -- LADDR+8 )
   8 0
   DO
      VLCOUNT 8BITS .R SPACE
   LOOP ;
: VLDUMP  ( LADDR CNT -- )
   SETWIDTH  0
   DO
      CR 2DUP 32BITS UD.R
      2 SPACES  2DUP
      (VLDUMP) SPACE (VLDUMP) 2DROP
      2 SPACES 16 0
      DO
         VLCOUNT QEMIT
      LOOP
      DONE? ?LEAVE
   16 +LOOP
   2DROP CR ;
: CMOVE>LV  ( ADR VLADR CNT -- )
   >R ROT R> 0
   DO
      COUNT 2OVER I 0 D+ VLC!
   LOOP
   2DROP DROP ;
: CMOVELV>  ( VLADR ADR CNT -- )
   0
   DO
      I 2OVER 0 -ROT D+ VLC@
      OVER I + C!
   LOOP
   DROP 2DROP ;

I realize that the REU could be accessed directly, rather than going through the block system. Part of the reason I do this on a simulated Commodore 64 is the nostalgia. Back in the day, not everyone had a Ram Expansion Unit. Some may have had more than one disk drive. Both versions of >VIRTUAL and >LVIRTUAL can be changed to access one of the disk drives for virtual memory by replacing RAM with the phrase
9 DR+ to access drive nine or
10 DR+ to access drive ten.


Top
 Profile  
Reply with quote  
PostPosted: Tue Jun 08, 2021 1:56 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890
JimBoyd wrote:

The code for VMSAVE is somewhat convoluted just prior to and within the DO LOOP . When writing virtual memory to disk, I was trying to avoid opening and closing a channel for each byte written and it got a little messy. I plan to test a new version of VMSAVE to see how opening and closing the channel for each byte affects the time to save memory.


To deal with the Commodore 64 kernal using the same kernal routine to send a byte to the screen or any I/O device, I/O words in Fleet Forth, such as EMIT , TYPE , and the disk block read write words normally direct output to the desired device, transfer the data, and clear the channel. This version of VMSAVE did things a little differently to save time. I ran a test with a version of VMSAVE that did as other Fleet Forth I/O words. It directed output to the disk, emitted a byte, then cleared the channel. This version took about one and a half times as long to transfer 8k from virtual memory to a program file on one of the disk drives.
Since these listings are for a Commodore 64, just read each double slash // as a backslash \
Code:
// VMSAVE -- REALLY SLOW
: DDEMIT  ( C -- )
   1 CHKOUT ?IO DEMIT CLRCHN ;
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   SWAP DUP SPLIT SWAP
   DDEMIT DDEMIT
   ?DO
      I VC@  DDEMIT
   LOOP
   1 CLOSE ;

I even used a very fast version of I/O error checking, ?IO .
Code:
// ERROR CODES FROM C64 KERNAL
// ARE ONE BYTE
CODE ?IO  ( EC -- )
   0 ,X LDA
   0= IF
      POP JMP
   THEN
   >FORTH
   IORESET IOERR ;


This next version of VMSAVE uses DTYPE to send up to 1k of data directly from the block buffer to the program file being created, rather than sending one byte at a time.
Code:
// VMSAVE
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   OVER -
   OVER  SPLIT SWAP
   1 CHKOUT ?IO  DEMIT DEMIT CLRCHN
   BEGIN
      OVER $3FF AND B/BUF SWAP -
      OVER UMIN 2PICK >VIRTUAL OVER
      1 CHKOUT ?IO DTYPE CLRCHN
      /STRING ?DUP 0=
   UNTIL
   DROP 1 CLOSE ;

After opening a data channel to the disk, it converts the starting address and ending address plus one to the starting address and length. Since each kilobyte of virtual memory aligns on a block buffer boundary, the address is ANDed with $3FF and subtracted from B/BUF ( $400 ) . The unsigned minimum of this and the count is the amount to send. /STRING uses this unsigned minimum to adjust the starting address and the count. When the count reaches zero, all of the virtual memory from the starting address to the ending address has been sent.
Although the primary purpose of DTYPE is to send data to one of the disk drives, it does not redirect output. The only difference between DTYPE and TYPE are, DTYPE is a primitive and it does not update CHARS and LINES , the two variables used for output formatting.
In the line with DTYPE , output is directed to the disk drive before DTYPE and the channel cleared after, in keeping with how Fleet Forth I/O works.
This version of VMSAVE takes only forty four percent as much time as the original.

The same technique used by this version of VMSAVE , handling up to 1k at a time and directly accessing this data in the block buffer, could also be used to speed up CMOVE>V and CMOVEV> , as well as their long virtual address counterparts, if these words are used to move large amounts of data. In the metacompiler, they normally move a small amount, such as a name or small string, from HERE to THERE .


Top
 Profile  
Reply with quote  
PostPosted: Tue Jul 06, 2021 2:14 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've been working with Fleet Forth's multitasker again. One of the difficulties with the multitasker is the word EXPECT . It is a primitive in Fleet Forth, but even if it was high level, it would still present difficulties. EXPECT , actually its vector, (EXPECT) , calls the C64 Kernal routine CHRIN at $FFCF. EXPECT is a deferred word.
Each call to CHRIN returns a single character, but CHRIN handles keyboard input differently than other sources. If the keyboard is the input source, CHRIN echoes the characters received to the screen until the return key is pressed. Once the return key is pressed, each call to CHRIN returns a character by reading it from the current logical line on the screen (the one with the cursor).
The advantage is that this is how the Commodore 64 screen editor works. It is possible to scroll to any line and type return. That line will be read in as if it had just been typed followed by a carriage return.
The disadvantage is that CHRIN will return nothing until the return key is pressed. It will not even return! It will keep running, echoing characters to the screen ( including characters generated by the scroll keys ), waiting for a carriage return.
This means that cooperative round robbin multitasking will not work.
My first work around was to write a multitasker version of (EXPECT) called (MEXPECT) . (MEXPECT) started as a primitive. If the input source was NOT the keyboard then (MEXPECT) jumped to the body of (EXPECT) . If input was from the keyboard, the jump to the body of (EXPECT) was branched over. (MEXPECT) transitioned to high level and performed the function of the screen editor in high level Forth.
This was not an elegant solution. (MEXPECT) was large enough that I didn't want to include it in the kernel as a better version of (EXPECT) . It was switched in by the word MULTI and switched back out by the word SINGLE . (MEXPECT) wasn't even a proper vector for EXPECT since I/O redirection could change which word EXPECT executed.
The multitasker word SINGLE , which is defined in Fleet Forth's kernel so it is always available, had two jobs:
1) set the deferred word PAUSE to NOOP , a no operation word.
2) set the code field of (EXPECT) to point to its own body.
The multitasker word MULTI had three jobs:
1) make sure the task executing MULTI is awake.
2) set the deferred word PAUSE to (PAUSE) , the task switcher.
3) set the code field of (EXPECT) to point to the body of (MEXPECT) . This worked since they both start out as code words.

I have since worked out a better solution: trick CHRIN.
The new version of (MEXPECT) still tests to see if input is from the keyboard and jumps to the body of (EXPECT) if it isn't. If input is from the keyboard, (MEXPECT) prints a carriage return, if necessary, to start a new line. It still echoes the characters to the screen until it receives a carriage return. It then sets location $D3 to zero to indicate the first column. It also sets $D4 to zero to clear quote mode. It then stuffs a carriage return into the keyboard buffer and executes (EXPECT) . CHRIN reads the carriage return and dutifully returns characters from the current logical line on the screen. The logical line could be 40 columns or 80 columns, the C64 Kernal maintains a table for that.
Code:
CODE (MEXPECT)  ( ADR CNT -- )
   $99 LDA
   0= NOT IF
      ' (EXPECT) >BODY JMP
   THEN
   >FORTH
   COLS ?CR
   BEGIN
      PAUSE  KEY DUP 13 <>
   WHILE
      DEMIT
   REPEAT
   $277 C!  1 $C6 C!  $D3 OFF
   (EXPECT) ;

This version of (MEXPECT) is small enough that I am considering making (EXPECT) headerless and including (MEXPECT) in the Fleet Forth kernel as (EXPECT) .
With the other changes to the Fleet Forth kernel to support it, such as simplifying SINGLE , it would only add about 45 bytes to Fleet Forth's kernel.

[Edit: fixed an off by one error in my count]


Top
 Profile  
Reply with quote  
PostPosted: Sun Jul 11, 2021 11:02 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've made the changes to my latest Fleet Forth kernel. The extra cost of 45 bytes would have been if I had removed the CR in QUIT and replaced the phrase COLS ?CR in the new (EXPECT) with CR .
I left those in place which should have made the new kernel 49 bytes bigger than it was before the new version of (EXPECT) .
Rather than make the old version of (EXPECT) a headerless word in the kernel, I incorporated it into the new version of (EXPECT) . The new Fleet Forth kernel is now only 42 bytes bigger with this new version of (EXPECT) .
Code:
CODE (EXPECT)  ( ADR CNT -- )
   $99 LDA
   0= IF
      >FORTH
      COLS ?CR
      BEGIN
         PAUSE  KEY DUP 13 <>
      WHILE
         DEMIT
      REPEAT
      // STUFF CR IN KBD BUFFER
      // AND SET CURSOR TO COLUMN 0
      $277 C!  1 $C6 C!  $D3 OFF
      >ASSEM
   THEN
   2 # LDA  SETUP JSR
   XSAVE STX  SPAN 1+ STY
   BEGIN
         N CPY
      0= WHILE
         N 1+ DEC
      0< NOT WHILE  CS-ROT
      THEN
         $FFCF JSR  // CHRIN
         13 # CMP
      0= NOT WHILE
         N 2+ )Y STA  INY
      CS-DUP 0= UNTIL
      N 3 + INC  SPAN 1+ INC
   AGAIN
   CS-SWAP
   THEN
      $99 LDA
      0= IF
         BEGIN
            $FFCF JSR  // CHRIN
            13 # CMP
         0= UNTIL
      THEN
   THEN
   SPAN STY
   XSAVE.NEXT JMP  END-CODE
' (EXPECT) IS EXPECT

There is an odd bug in the Commodore 64's CHRIN Kernal routine. The Commodore 64's I/O routines can be used with the keyboard and screen or I/O devices. The default is reading from the keyboard and writing to the screen. When using this routine to read the keyboard (actually, using the C64 screen editor), there is an edge case. Under certain conditions, If less than the number of characters on the logical line are read before writing to the screen, there will be odd cursor behavior. The following section of code in (EXPECT) consumes all characters after the requested amount until a carriage return is received. This eliminates the bug and is only done when using the C64 screen editor (when the input source is the keyboard) so will not affect using (EXPECT) to read from other sources, such as the disk drives.
Code:
      $99 LDA
      0= IF
         BEGIN
            $FFCF JSR  // CHRIN
            13 # CMP
         0= UNTIL
      THEN

With the new version of (EXPECT) , SINGLE is now just:
Code:
: SINGLE  ( -- )
   ['] NOOP IS PAUSE ;

That takes care of the multitasker support built into Fleet Forth's kernel. Now I need to clean up the rest of the multitasker source.


Top
 Profile  
Reply with quote  
PostPosted: Sun Jul 25, 2021 9:11 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've briefly mentioned Fleet Forth's cold start and warm start routines in previous posts. I'll try to explain them in this post.
When loading and running Fleet Forth, the cold start routine starts with Fleet Forth's one line of BASIC:
Code:
10 SYS XXXXX

where XXXXX is the address of the cold start routine. There is a subroutine used by both the cold start and warm start routines. It is headerless and has no code field since it is a subroutine, but it has the label (WARM) in the Fleet Forth source.
Here is the source for (WARM) :
Code:
HEX
HSUBR (WARM)
   SEI  CLD
   2F # LDA  0 STA
   36 # LDA  1 STA
   USER.DATA    LDA  UP    STA
   USER.DATA 1+ LDA  UP 1+ STA
   6C # LDA  W 1- STA
   'THERE USER.DATA - 1- # LDY
   BEGIN
      USER.DATA ,Y LDA  UP )Y STA
      DEY
   0< UNTIL
   CLI
   >FORTH
   SP! AP! [COMPILE] [
   IORESET SINGLE DECIMAL
   PAGE BOOTCOLORS
   >ASSEM
   RTS
END-CODE

The subroutine (WARM) has a mix of low and high level code.
It first disables maskable interrupts and clears the decimal mode. The memory map for Fleet Forth is set (switching out the BASIC rom) and the opcode for an indirect jump is stored at address W-1.
The label USER.DATA points to an area just after the BASIC fuse but before the first Forth word. This area holds the bootup, or startup, values for the first nine user variables. The first value is the startup value of the first user variable and the value for the Forth virtual machine register UP , the user pointer. It is copied to UP then the startup value for the first eight user variables are copied to the user area.
The first three are unnamed. They are reserved for the multitasker, which is not a part of Fleet Forth's kernel.
The next five are:
RP0 The address of the base of the return stack.
SP0 The address of the base of the data stack.
SPLIM The limit or "full mark" for the data stack. Since the data stack is only checked for overflow (and underflow) by the word ?STACK , which is normally only used by INTERRPET , there are fourteen cells available past this point.
AP0 The address of the base of the auxiliary stack.
APLIM The limit of the auxiliary stack. Since the words to move items to and from the auxiliary stack perform bounds checking on the auxiliary stack, this limit is a "hard" limit.

This subroutine then transitions to high level Forth and executes the following words:
SP! Clears the data stack. This word is a primitive that uses the value from the user variable SP0 to initialize the data stack.
AP! Clears the auxiliary stack using the value in AP0 .
[ Sets the Forth state to interpreting.
IORESET Resets the deferred words used for I/O to their default values.
SINGLE Switches off multitasking by setting PAUSE to a no-op.
DECIMAL Sets the number base to decimal.
PAGE Clears the screen.
BOOTCOLORS Sets the screen colors to their bootup values.
And finally >ASSEM causes a transition back to low level for an RTS back to the caller.

Here is the source for Fleet Forth's word COLD:
Code:
HEX // USER COLD START
: COLD  ( -- )
   SAVE-BUFFERS
   ACLOSE     // CLOSE ALL FILES
   >ASSEM
   // POWERUP COLD START
   // PATCH THE BASIC FUSE
   DECIMAL HERE 0 <# #S #>  HEX
   FUSE  OVER - SWAP CMOVE>V
   SEI  CLD  FF # LDX  TXS
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FFE7 JSR  // CLALL
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT
   WARM  SPLIT SWAP
      # LDA  $300 STA
      # LDA  $301 STA
   (WARM) JSR
   >FORTH
   EMPTY 0 DRIVE CONFIGURE
   #12 SPACES
   [ HERE #18 + >A ]
   ." C64 FLEET FORTH  COPYRIGHT (C) 1995-2021 BY JAMES BOYD "
   [ HERE 1- >A ]
   INITIAL QUIT ; -2 ALLOT
   $D A> C!  $D0D A> !

COLD is used from within Fleet Forth to perform a cold start.
It saves the contents of the block buffers with SAVE-BUFFERS and closes all open files with ACLOSE .
COLD then transitions to a code word and flows into the cold start routine.
This section of the source:
Code:
   DECIMAL HERE 0 <# #S #>  HEX
   FUSE  OVER - SWAP CMOVE>V

patches the one line of BASIC so the SYS instruction points to the cold start routine.
In the cold start routine interrupts are disabled, the processor stack is initialized and some Commodore 64 Kernal routines are called. I performed an experiment by replacing those five JSR instructions with fifteen NOP instructions and saving the modified Forth system. The test system loaded and ran just fine. This is why those five C64 kernal calls are there. Some of the C64 Kernal routines are vectored through page 3 so that the user can replace these routines or add functionality. This added functionality can be in code that is stored in the C64 tape buffer or in a range of RAM from $C000 TO $CFFF (49152 to 53247). Fleet Forth uses both of these memory areas so the C64 Kernal routines are called to reset RAM and reset these vectors to their default values.

$FF87 RAMTAS Clears pages 0 2 and 3 to zeros. It also performs a nondestructive test of memory to set the Kernal bottom and top of memory pointers and sets screen memory to $400.
$FF8A RESTOR As its name implies, restores the default C64 Kernal vectors in page 3.
$FFE7 CLALL Although this routine's name implies that it closes all, it merely sets the count of open files to zero at address $98 and falls through to the C64 kernal routine CLRCHN, which clears the channels and sets the keyboard as the input device and the screen as the output device.
$FF84 IOINIT This routine initializes the Complex Interface Adapter chips, sets CIA #1 to generate an interrupt via timer A every sixtieth of a second, and turns off the volume of the SID chip.
$FF81 CINT Initializes the screen editor and the VIC chip.

The cold start routine then stores the address of the warm start routine at addresses $300-$301 (768-769). The warm start routine will execute when the STOP and RESTORE keys are pressed. The RESTORE key is hardwired to generate a non-maskable interrupt. This is a convenient way to get out of an infinite loop without the need to reset the machine. A BRK instruction executed will also cause a Fleet Forth warm start.

The cold start routine calls (WARM) then transitions to high level Forth and executes the following:
EMPTY Empties the dictionary to its latest empty point. It accomplishes this by fetching the startup value for the ninth user variable with a value in the boot area, DP , the dictionary pointer. EMPTY stores a copy of this value in the variable FENCE before branching into FORGET . FORGET does the "heavy lifting" of removing all vocabularies defined after the "forget point" and pruning the remaining vocabularies.
0 DRIVE Sets the default drive to device 8.
CONFIGURE Initializes the block buffer system with all buffers empty.
The cold start routine prints the startup message before executing the word INITIAL and then QUIT .
INITIAL is a deferred word which is normally set to the no-op NOOP .

Here is the source for Fleet Forth's warm start routine:
Code:
HSUBR WARM
   $FF # LDX  TXS
   (WARM) JSR
   >FORTH
   FORTH DEFINITIONS
   ." RWARM START."
   ABORT ; -2 ALLOT

The processor stack is initialized and the subroutine (WARM) is called. The warm start routine then transitions to high level. It sets the CONTEXT and CURRENT search orders to FORTH , displays the warm start message in reverse video and aborts.

The biggest difference between the cold start and warm start routines for a programmer is this: The warm start routine will not empty the dictionary, alter the default disk drive or empty the block buffers.


Top
 Profile  
Reply with quote  
PostPosted: Sun Sep 19, 2021 8:26 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've cleaned up Fleet Forth's multitasker source. Fleet Forth has a round robin cooperative multitasker, although another type could be written. It only has support for the one main task with background tasks. Each task uses a portion of page one for its return stack and a portion of the area from address $02 to address $7E in page zero for its data stack. A task is a CREATE word, like a variable but with a larger parameter field. A task's user area is in its parameter field. A task's parameter field may also have room for a task to have its own PAD .
In this post, any mention of a task's return stack refers to the portion of the return stack used by the task. The same goes for mention of a task's data stack.
Here is the multitasking lexicon:
Code:
(PAUSE)  ( -- )
ENTRY  ( -- ADR )
READY  ( -- ADR )
TOS  ( -- ADR )
MULTI  ( -- )
SINGLE  ( -- )
WAKE  ( TADR -- )
SLEEP  ( TADR -- )
LOCAL  ( TADR ADR1 -- ADR2 )
       ( ADR1 TADR -- ADR2 )
STOP  ( -- )
LINK-TASK  ( TASK -- )
UNLINK-ALL  ( -- )
UNLINK-TASK  ( TASK -- )
TASK  ( U SP0 RP0 -- )
ACTIVATE  ( TASK -- )

(PAUSE) is the task switcher. Here is the source:
Code:
CODE (PAUSE)  ( -- )
   IP 1+ LDA  PHA       // IP ON RET
   IP LDA  PHA          //
   TXA  PHA             // SP ON RET
   TSX  TXA             // RP STORED
   4 # LDY  UP )Y STA   // IN TOS
   BEGIN
      0 # LDY
      UP )Y LDA  TAX  INY
      UP )Y LDA  UP 1+ STA  INY
      UP STX  UP )Y LDA
   0= NOT UNTIL
   4 # LDY  UP )Y LDA  TAX  TXS
   PLA  TAX
   ' EXIT @ JMP  END-CODE

The next three words, ENTRY , READY , and TOS are user variables with offsets 0, 2, and 4 respectively. These offsets are reserved in the Fleet Forth kernel for multitasking.
ENTRY points to the beginning, or entry, of the user area for the next task. READY has the logic value true if the task is "awake" and ready to go or false if it is "asleep", although (PAUSE) only checks the low byte. TOS is used to store a task's return stack pointer.
(PAUSE) saves the value of IP to the task's return stack as well as the value of the current task's data stack pointer. The return stack pointer is saved in the user variable TOS . (PAUSE) then follows ENTRY to the next task, testing READY until it finds a task which is awake. Once a task which is awake is found, its return stack pointer is fetched from TOS and stored in the S register and the data stack pointer is restored from the task's return stack. A jump to EXIT fetches the saved value of IP from the task's return stack and returns to NEXT to resume execution of the Forth thread for this task.
Code:
0 USER ENTRY   2 USER READY
4 USER TOS
: MULTI  ( -- )
   READY ON
   ['] (PAUSE) IS PAUSE ;
: WAKE  ( TADR -- )  2+ ON ;
: SLEEP  ( TADR -- )  2+ OFF ;
: LOCAL  ( BASE ADR1 -- ADR2 )
   [ ASSEMBLER ] UP [ FORTH ]
   @ - + ;
: STOP  ( -- )
   BEGIN
      READY OFF  PAUSE
   AGAIN ; -2 ALLOT

MULTI assures the task executing it is "awake" (normally the main task) and sets the deferred word PAUSE to (PAUSE) . The counterpart of MULTI is SINGLE . It is the only word in the multitasker lexicon defined in Fleet Forth's kernel. SINGLE sets PAUSE to NOOP a no operation word. NOOP is a word without a parameter field. Its code field points to NEXT .
WAKE takes a task's address and wakes it up.
SLEEP takes a task's address and puts it to sleep.
LOCAL takes a task's address and the address returned by a user variable in the currently executing task (usually main) and returns the address of that user variable in the given task. For example, suppose there is a task named DUMMY1 . I can tell if DUMMY1 is awake or not with the phrase:
Code:
DUMMY1 READY LOCAL ?

where ? displays the contents of an address. LOCAL has two stack comments because it doesn't matter whether it is used with the user variable address on top or the task address, it returns the same result.
STOP is used for a one-shot task, one which does not have an infinite loop. STOP puts the task to sleep then pauses. STOP has an infinite loop in the event that a stopped task is woken up inadvertently. The task will put itself back to sleep and pause the next time it runs.

Linking and unlinking:
Code:
: LINK-TASK  ( TASK -- )
   DUP SLEEP
   ENTRY 2DUP @ SWAP! ! ;
: UNLINK-ALL  ( -- )
   [ ASSEMBLER UP @ ] LITERAL
   DUP UP !  ENTRY ! ;
: UNLINK-TASK  ( TASK -- )
   [ ASSEMBLER ] UP [ FORTH ]
   BEGIN
      @ DUP @
      [ ASSEMBLER ] UP [ FORTH ] @ =
      IF  2DROP EXIT  THEN
      DUP @ 2PICK =
   UNTIL
   SWAP @ SWAP! ;

LINK-TASK takes a task's address and puts it to sleep because a newly created task does not have anything to do and would "crash and burn" if awake. It then links the task into the round robin list of tasks. It is important that a task does not get linked into the round robin list more than once!
UNLINK-ALL , as its name implies, unlinks all the tasks by setting UP , Forth's user pointer, to the main task and setting ENTRY for the main task to point to itself. The ability to unlink all the tasks makes it safe to forget a task. I've read somewhere that once a task is created, it should not be forgotten. It seems to me that this would make prototyping harder with the need to leave the Forth system and reload it to remove background tasks. To provide a little more protection, FORGET switches off multitasking. If the need to forget a Forth word arises, one or more tasks may be forgotten in the process. It would be prudent to unlink all tasks before switching multitasking back on and link them back into the round robin list one at a time. A cold or warm start will unlink all tasks as a side effect of setting up the user area and user pointer. ABORT or any word which executes ABORT switches multitasking off.
UNLINK-TASK takes the address of a task and will unlink just that task from the round robin list of tasks. If the address on the stack is not the address of a task linked in the round robin list, UNLINK-TASK will, after traversing the list, exit without changing anything.

Task creation and activation:
Code:
: TASK  ( U SP0 RP0 -- )
   CREATE
      HERE RP0 LOCAL !
      HERE SP0 LOCAL !
      // OPTIONAL
      10 HERE BASE LOCAL !
      HERE #USER + HERE DP LOCAL !
      10 UMAX ALLOT ;
: ACTIVATE  ( TASK -- )
   DUP WAKE
   R> OVER RP0 LOCAL @ 1- DUP>R !
   DUP SP0 LOCAL @ R@ 1- C!
   TOS LOCAL R> 2- SWAP! ;

As I said, TASK is a CREATE word, it returns the address of its parameter field. The definition of TASK has been changed. The original definition took three parameters, the size of the user area for the task being created, the size of its portion of data stack, and the size of its portion of return stack. There were some constants and values used to keep track of how much of the stacks were set aside for each task. With the ability to forget tasks, this would necessitate resetting the values used to keep track of the portion of the stacks set aside for the background tasks.
The new version of TASK also takes three parameters. The size of the user area for the task, the address of the base of the data stack for the task being created, and the address of the base of its return stack.
A task's parameter field will have room for at least five user variables, possibly more. The five required user variables are:
ENTRY , READY , TOS , RP0 , and SP0 . RP0 holds the address of the base of the return stack and SP0 holds the address of the base of the data stack. When a stack is created, its value of BASE is set to ten and its DP , dictionary pointer, is set to the address after the latest user variable.
ACTIVATE takes the address of a task and is used in a word to give a task something to do. Here is an example with two tasks. They both flash the border, but at different intervals.
Code:
180 VALUE DELAY1 175 VALUE DELAY2
0 $1C $12F TASK DUMMY1
0 $38 $15F TASK DUMMY2
: FLASH1
   DUMMY1 ACTIVATE
   BEGIN
      $D020 C@ 1+ BORDER
      DELAY1 JIFFIES
   AGAIN ;
: FLASH2
   DUMMY2 ACTIVATE
   BEGIN
      $D020 C@ 1+ BORDER
      DELAY2 JIFFIES
   AGAIN ;

Code:
SEE FLASH1
FLASH1
 7DB4 7D84 DUMMY1
 7DB6 7CB2 ACTIVATE
 7DB8  A35 LIT D020
 7DBC 1298 C@
 7DBE 11BF 1+
 7DC0 455F BORDER
 7DC2 7D66 DELAY1
 7DC4 42A6 JIFFIES
 7DC6  BD0 BRANCH 7DB8
16
 OK

FLASH1 is executed from the main task. ACTIVATE wakes the task DUMMY1 . It then places the address $7DB8 on the task's return stack. The data stack pointer for DUMMY1 is then placed on its return stack. The adjusted return pointer for DUMMY1 is stored in its version of TOS . DUMMY1 is now ready for the Forth thread at address $7DB8 to run.


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 7:32 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I posted about Fleet Forth's cold and warm start routines here.
There was an error in the cold start routine in the section of C64 Kernal calls which has since been corrected.
Code:
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FFE7 JSR  // CLALL
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT

The Commodore 64 Kernal routine CLALL clears the table of open files, but it also falls through to the CLRCHN routine. This routine restores the default input device (the keyboard) and the default output device (the screen). It will also send an UNTALK or UNLISTEN command if necessary. The problem is that the C64 routine RAMTAS clears the memory locations holding the necessary information.
Here is the corrected list of C64 Kernal calls.
Code:
   FFE7 JSR  // CLALL
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT

I've never had a problem with this because I load and run Fleet Forth immediately after powerup, or in recent years immediately after starting the VICE C64 simulator.


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 8:47 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

Fleet Forth has a word, DIGIT , which is not in the Forth-83 standard, but it is in Fig Forth, F83 and F-PC. DIGIT is a primitive which attempts to convert a character into a valid digit in the specified number base and returns the result and a flag.
In Fig Forth, DIGIT has the following stack effect.
Code:
DIGIT  ( CHAR BASE -- N TRUE )
       ( CHAR BASE -- FALSE )

However, in F83 and F-PC it has this stack effect.
Code:
DIGIT  ( CHAR BASE -- N TRUE )
       ( CHAR BASE -- CHAR FALSE )

I've also seen this stack effect where accessing the value of BASE is built in.
Code:
DIGIT  ( CHAR -- N TRUE )
       ( CHAR -- CHAR FALSE )

Which version would be best for a Forth-83 Standard system such as Fleet Forth?
Of the first two versions, the Fig Forth version has the advantage that CONVERT does not need to discard the unneeded character when a nonconvertible character is encountered, thus saving one cell. On the other hand, the F83 and F-PC version could be more generally useful since it always returns two stack items. Here is an example from Inside F83 by C. H. Ting where the result of the conversion is not needed.
Code:
: DIGIT? ( char -- f )
   BASE @ DIGIT
   NIP ;

The third version is the one I'm leaning toward. The built in access of BASE saves two cells in each word which uses DIGIT , making that word slightly smaller and faster. This change only makes DIGIT two bytes longer comparing the value in the accumulator with the value of BASE
Code:
   ' BASE >BODY C@ # LDY
   UP )Y CMP

versus comparing it with the value on the top of the data stack.
Code:
   0 ,X CMP

DIGIT? would then be
Code:
DIGIT?  ( char -- f )
   DIGIT NIP ;


[Edit: Fixed some typos]


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 11:15 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8510
Location: Southern California
I thought I'd look up what Starting Forth has to say about DIGIT since it often has foot notes telling the differences in figForth, Forth-79, Forth-83, etc.; but I was shocked to see it wasn't even there! I have DIGIT> which has the stack effect you show first.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 12, 2021 1:04 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I've decided to change Fleet Forth's word DIGIT to have the third stack effect mentioned in my previous post.
Code:
DIGIT  ( CHAR -- N TRUE )
       ( CHAR -- CHAR FALSE )

Code:
CODE DIGIT  ( CHAR -- N TRUE )
            ( CHAR -- CHAR FALSE )
   SEC  0 ,X LDA  $30 # SBC
   ' FALSE @ CS NOT BRAN
   $A # CMP
   CS IF
      7 # SBC  $A # CMP
      ' FALSE @ CS NOT BRAN
   THEN
   ' BASE >BODY C@ # LDY
   UP )Y CMP
   ' FALSE @ CS BRAN
   0 ,X STA
   ' TRUE @ JMP END-CODE

DIGIT is a primitive and including the access of the value in BASE only makes it two bytes bigger than before. This change saves two cells, or four bytes, in CONVERT , the high level word which uses DIGIT .
Returning two stack items even when conversion is unsuccessful adds a cell to CONVERT for a DROP to discard the unconverted character when it branches out of its loop.
Code:
: CONVERT  ( D1 ADR1 -- D2 ADR2 )
   1+
   BEGIN
      COUNT DIGIT         \ count base @ digit
   WHILE
      2SWAP BASE @ *
      SWAP BASE @ UM* D+
      ROT
      DPL @ 0< 1+ DPL +!
   REPEAT
   DROP 1- ;              \ 1- ;

The comments show the original lines.
As I mentioned in my previous post, having DIGIT always return two stack items makes it more generally useful. Although this change will make CONVERT two bytes bigger, if I only intended DIGIT to be used by CONVERT , I would have made it headerless.


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 12, 2021 2:46 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 890

I mention Fleet Forth's word NUMBER? here. Back then it was named -NUMBER . I've managed to reduce the size of its body to 132 bytes.
I've tried two other versions. One is 122 bytes but requires the first character (after any leading conversion base specifier or minus sign) to be a valid digit. The following would all be valid numbers with this version:
Code:
$0.1234
%234.5.8.
#-42.3
123.

And these would not be valid:
Code:
$.1234
.1234
0.1..2
123...45

The other version also requires the first digit after any conversion specifier or minus sign to be a valid digit. It is only 116 bytes because it allows more than one valid non-digit punctuation character in a row. Here are examples of valid numbers with this version:
Code:
$0.123.....56.
123...45.2..
#-137....2



Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 354 posts ]  Go to page Previous  1 ... 10, 11, 12, 13, 14, 15, 16 ... 24  Next

All times are UTC


Who is online

Users browsing this forum: No registered users and 11 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: