Page 13 of 25
Re: Fleet Forth design considerations
Posted: Thu Jun 03, 2021 2:40 am
by JimBoyd
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: Select all
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: Select all
: 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: Select all
: 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:
Another approach is modifying >VIRTUAL .
Code: Select all
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:
the next 64k blocks with:
and so on.
The third approach is to define a set of long virtual memory access words.
Code: Select all
: >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: Select all
: 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.
Re: Fleet Forth design considerations
Posted: Tue Jun 08, 2021 1:56 am
by JimBoyd
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: Select all
// 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: Select all
// 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: Select all
// 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 .
Re: Fleet Forth design considerations
Posted: Tue Jul 06, 2021 2:14 am
by JimBoyd
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: Select all
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]
Re: Fleet Forth design considerations
Posted: Sun Jul 11, 2021 11:02 pm
by JimBoyd
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: Select all
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: Select all
$99 LDA
0= IF
BEGIN
$FFCF JSR // CHRIN
13 # CMP
0= UNTIL
THEN
With the new version of (EXPECT) , SINGLE is now just:
Code: Select all
: 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.
Re: Fleet Forth design considerations
Posted: Sun Jul 25, 2021 9:11 pm
by JimBoyd
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:
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: Select all
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: Select all
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: Select all
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: Select all
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.
Re: Fleet Forth design considerations
Posted: Sun Sep 19, 2021 8:26 pm
by JimBoyd
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: Select all
(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: Select all
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: Select all
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:
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: Select all
: 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: Select all
: 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: Select all
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: Select all
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.
Re: Fleet Forth design considerations
Posted: Sun Oct 03, 2021 7:32 pm
by JimBoyd
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: Select all
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: Select all
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.
Re: Fleet Forth design considerations
Posted: Sun Oct 03, 2021 8:47 pm
by JimBoyd
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: Select all
DIGIT ( CHAR BASE -- N TRUE )
( CHAR BASE -- FALSE )
However, in F83 and F-PC it has this stack effect.
Code: Select all
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: Select all
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: Select all
: 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
versus comparing it with the value on the top of the data stack.
DIGIT? would then be
[Edit: Fixed some typos]
Re: Fleet Forth design considerations
Posted: Sun Oct 03, 2021 11:15 pm
by GARTHWILSON
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.
Re: Fleet Forth design considerations
Posted: Tue Oct 12, 2021 1:04 am
by JimBoyd
I've decided to change Fleet Forth's word DIGIT to have the third stack effect mentioned in my previous post.
Code: Select all
DIGIT ( CHAR -- N TRUE )
( CHAR -- CHAR FALSE )
Code: Select all
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: Select all
: 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.
Re: Fleet Forth design considerations
Posted: Tue Oct 12, 2021 2:46 am
by JimBoyd
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:
And these would not be valid:
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: Select all
$0.123.....56.
123...45.2..
#-137....2
Re: Fleet Forth design considerations
Posted: Wed Oct 20, 2021 11:28 pm
by JimBoyd
I have previously mentioned the word VALID? . This word is used to test if a non-digit character is valid punctuation for a number. It is a deferred word and by default is set to (VALID?) , which only recognizes the period ( . ) as valid punctuation in a double number. Since VALID? is a deferred word, what is accepted as valid punctuation for a number can be changed. Starting Forth mentions these as valid punctuation for a double number:
Some of these, as well as other punctuation, could be added by using a new version of (VALID?) . Taking this into consideration, I have decided to use the version of NUMBER? which requires valid non-digit punctuation in a number be separated by at least one digit. I believe a typo would be less likely slip by as a valid number with the tighter restriction and that it is well worth the extra six bytes.
My fellow Forthwrites, am I being a bit too cautious? Is it worth the extra six bytes?
Re: Fleet Forth design considerations
Posted: Wed Oct 20, 2021 11:47 pm
by barrym95838
I say it's worth it, but you should just follow your gut ... any decision you make can be easily revised later.
Re: Fleet Forth design considerations
Posted: Thu Oct 21, 2021 12:40 am
by JimBoyd
I say it's worth it, but you should just follow your gut
Thanks.
This was one of those decisions where I could have gone either way, but leaned toward the more restrictive, and slightly larger, version of NUMBER? .
Re: Fleet Forth design considerations
Posted: Thu Oct 21, 2021 1:58 am
by JimBoyd
I have previously made brief mention of Fleet Forth's word WHERE . It is used to display the location of an error when (ABORT") aborts with an error message. WHERE can be useful apart from (ABORT") to display an error message which is not known beforehand.
-SET is one example:
Code: Select all
: -SET ( -- )
WHERE
R@ 2- @ >NAME CR RON ID. ROFF
." NOT SET" ABORT ; -2 ALLOT
-SET is the vector for a new deferred word until a new vector is set with IS :
Code: Select all
' NOOP IS PAUSE
' (PAUSE) IS PAUSE
' (EMIT) IS EMIT
If the new deferred word, prior to being set to a new vector, is executed from the command line, -SET displays the odd message that EXECUTE is not set. When executed from within a high level Forth word, the message shows the name of the deferred word which is not set.
Another example is ?D , the word which checks the disk drive for an error condition after block read/write.
Code: Select all
: ?D ( -- )
$0F >SLF# (?D) 0EXIT
WHERE
CR ." DISK ERROR:" CR
DEB S? ABORT ; -2 ALLOT
WHERE displays where the error occurred. The message 'DISK ERROR:' is displayed, followed my the message in the Disk Error Buffer or DEB , which comes from the disk drive as a text string.
To get a better feel of what Fleet Forth error messages look like, here are some examples.
This one is an example of using PAD as a buffer for sector 10 of track 37 on disk 0 of the current drive to read (r/w flag value is 1) 256 bytes with the sector read/write word. Device 8 is a 1541 disk drive and does not have 37 tracks. I've added comments after the fact.
Code: Select all
8 DRIVE OK \ select device 8 as the current drive
DOPEN OK \ open current drive for block access
PAD 10 37 0 1 256 SR/W \ try to read sector 10 of track 37
PAD 10 37 0 1 256 SR/W \ this line shown by Fleet Forth in reverse video
^^^^
DISK ERROR:
66,ILLEGAL TRACK OR SECTOR,37,10
When WHERE shows the line in a screen or the text input buffer or an evaluated string, it shows that line in reverse video so it stands out. The carets ( ^ ) identify the text which caused the error.
Here are some errors due to (intentional) typos.
From the console:
Code: Select all
DECIMAL OK
CR 1 2 + . 123OOPS 4 5 * .
3
CR 1 2 + . 123OOPS 4 5 * .
^^^^^^^
WHAT?
and from a screen:
Code: Select all
1 RAM LOAD 7
SCR# 32769 LINE# 7
2 5 + . 321OOPS 7 9 * .
^^^^^^^
WHAT?
Missing delimiter for a string:
Code: Select all
: TEST2
CR ." START TEST2 TEST1
CR ." START TEST2 TEST1
^^
" MISSING
and using a deferred word which has not been set:
Code: Select all
DEFER PWM OK
: TEST 50 PWM ; OK
TEST
TEST
^^^^
PWM NOT SET
Here is a screen shot of trying to forget a word protected by FORGET's internal 'fence'.