: ALIGN ( ADR1 -- ADR2 )
HERE 1+ SPLIT DROP 0= ABS ALLOT ;
// DUMMY TARGET TO UNSMUDGE
HERE >A 0 C,
: :NONAME ( -- ADR )
ALIGN HERE DUP (IS) LAST
CURRENT @ CONTEXT !
[ A> ] LITERAL TRUE
[ ' : @ ] LITERAL , ] ;
This version of align doesn't align to an even address. It will allot one byte if the address is of the form $xxFF. The C64 uses the 6510 processor which has the infamous indirect jump bug.
Fleet Forth doesn't save the stack depth for colon or code words. The address to unsmudge and the value of true are pushed on the stack for compiler security. The one byte allotted prior to the definition of :NONAME provides a safe address for semicolon to unsmudge. Here are Fleet Forth's : and ; .
As for the ability to support recursion in a headerless word, Fleet Forth has the word VOCS to display the vocabulary tree. VOCS uses the recursive word .VOCS . .VOCS could be made headerless.
: CNONAME ( -- ADR ) // CODE DEF
:NONAME [COMPILE] [
ASSEMBLER [ ASSEMBLER ] MEM
LAST 2+ LAST ! ; // OVERWRITE CFA
Although I doubt I would need CNONAME . LAST is handy if there is an error while compiling. The following will not compile successfully. There is no matching THEN .
If the definition was compiled from the command line, there will not even be source to examine; however, there is a LAST resort to (SEE) what went wrong.
In stead of using :NONAME, can one not just use HERE, then write some code, leave HERE on the stack so that when some future word definition needs it, the address to return to is already there. You wouldn't even need to create a colon definition or use :NONAME.
:NONAME is used to create a headerless colon definition and some of the words in Fleet Forth's system loader are candidates. These colon definitions are complex enough that I would not want to rewrite them as code words, code words which would be much larger.
The 0 , would be needed since CODE does a -2 allot, which stores the CFA which points to the PFA (or body) of a regular word definition. The address for HERE gets left on the stack and is used, when needed, in the assembler calculation. Which I believe needs to be HERE+2 to skip past the CFA pointer.
Using CODE to create a headerless code word doesn't work because CODE parses the text stream for a name and creates a header. 64Forth's CODE behaves the same way.
0 FH LOAD
( EXISTS
SCR# 32769 LINE# 1
HERE 0 , CODE ( N -- N+10 )
^^
WHAT?
So CODE tries to redefine open parenthesis and the system aborts when double dash can't be found in the specified search order. Not surprising since double dash isn't defined in Fleet Forth.
One way to create a headerless code word in Fleet Forth would be the following.
ASSEMBLER MEM ALIGN // NAMELESS CODE DEF -- INCONVENIENT
HERE DUP 2+ ,
$D020 INC
NEXT JMP
FORTH
I should mention that ALIGN does not align to an even address. If the address is of the form $xxFF, ALIGN allots one byte to push the address past the page boundary. This is done to avoid the indirect jump bug found in NMOS 6502s and the C64's 6510 processor.
Here is a way to create headerless subroutines in Fleet Forth. There is no CFA, just an address to to be used by JSR.
I've been entertaining some ideas on changing the working of INTERPRET which breaks the way INTERPRET would exit the interpret loop.
I already mentioned that Fleet Forth's WORD parses the text stream and returns the address of a counted string padded with a trailing blank as per the Forth-83 Standard. The address it returns is HERE . If the text stream is exhausted as WORD is called, then a count of zero followed by a trailing blank will result.
The find primitive, when checking a header for a match, checks the count and each character, using exclusive or, until it finds a non-match. The high bit of the last comparison is masked off ( $7F and). If this results in a match, the word was found. If not, follow the link to the next header, unless the link field has a value of zero. This is the shortest and fastest way I could write the search with a traditional dictionary with headers next to their respective code fields. Because of this behavior, if there is a name in the dictionary with just a count of zero and a blank space (both having the high bit set), it will be found when WORD is called on an empty text stream and the returned address is passed to FIND .
This was the means used to exit the interpreter in Fleet Forth. The mystery word was an immediate alias for EXIT which had no parameter field, just a code field which pointed to the body of EXIT .
My latest changes to Fleet Forth did away with that. The mystery word is now gone and INTERPRET checks the size of the string returned by WORD . If the size is zero, INTERPRET exits.
Here is the new INTERPRET .
// INTERPRET
: INTERPRET ( -- )
BEGIN
PAUSE NAME C@ 0EXIT
HERE FIND ?DUP
IF
STATE @ =
IF
,
ELSE
EXECUTE ?STACK
THEN
ELSE
NUMBER? ?HUH ?STACK
DPL @ 0<
IF
DROP STATE @
IF
[COMPILE] LITERAL
THEN
ELSE
STATE @
IF
SWAP
[COMPILE] LITERAL
[COMPILE] LITERAL
THEN
THEN
THEN
AGAIN ; -2 ALLOT
This version of INTERPRET is six bytes larger; however, removing the mystery word saves six bytes. So far, everything works fine.
This is what I'm going to try next:
: (I/C) ( ADR -- ? )
FIND ?DUP
IF
STATE @ =
IF , EXIT THEN
EXECUTE ?STACK EXIT
THEN
NUMBER? ?HUH ?STACK DPL @ 0<
IF
DROP STATE @ 0EXIT
[COMPILE] LITERAL EXIT
THEN
STATE @ 0EXIT SWAP
[COMPILE] LITERAL
[COMPILE] LITERAL ;
This latest modification will allow me to remove some of the 'tricky code' from the metacompiler. I wrestled with this for a while because the metacompiler is not a part of Fleet Forth for normal day to day use. It's more like an application written in Fleet Forth to build new versions of the kernel. The metacompiler is already large and reducing a little complexity in it at the expense of even a little bit larger Fleet Forth didn't seem worthwhile. I'm investigating this newest version of Fleet Forth's interpreter not just for the benefit of the metacompiler, but because it will open up other possibilities as well.
[Edit: Minor correction. It's the metacompiler's complexity which could be reduced, not necessarily its size.]
Last edited by JimBoyd on Sun Dec 26, 2021 7:33 pm, edited 1 time in total.
: ALIGN ( ADR1 -- ADR2 )
HERE 1+ SPLIT DROP 0= ABS ALLOT ;
// DUMMY TARGET TO UNSMUDGE
HERE >A 0 C,
: :NONAME ( -- ADR )
ALIGN HERE DUP (IS) LAST
CURRENT @ CONTEXT !
[ A> ] LITERAL TRUE
[ ' : @ ] LITERAL , ] ;
There is a slight error in the stack comment for :NONAME . The actual stack comment should be
( -- ADR ADR2 TRUE )
ADR2 and TRUE are consumed by semicolon . ADR2 is the address of an unused byte, a safe target semicolon can 'unsmudge'. After semicolon executes, ADR, the address of the nameless word's code field, is the only one of the three parameters left on the stack
For comparison, here is the stack effect for colon.
( -- LATEST TRUE )
[Edit: corrected an error. EXIT does not consume anything from the stack. It pulls an address off the return stack and places it in IP .
: (I/C) ( ADR -- ? )
FIND ?DUP
IF
STATE @ =
IF , EXIT THEN
EXECUTE ?STACK EXIT
THEN
NUMBER? ?HUH ?STACK DPL @ 0<
IF
DROP STATE @ 0EXIT
[COMPILE] LITERAL EXIT
THEN
STATE @ 0EXIT SWAP
[COMPILE] LITERAL
[COMPILE] LITERAL ;
I've made this change to Fleet Forth and it works great. Since FIND is in (I/C) rather than INTERPRET , (I/C) and any other vector used with the deferred word I/C takes the string address returned by BL WORD .
I mentioned multi line comments and the word COMMENT: in another post. The drawbacks to this version of COMMENT: are that it is case sensitive in a case insensitive Forth and it only works within the current text stream. This is more than sufficient if loading source from blocks; however, Fleet Forth also has the ability to include source from Commodore 64 sequential files (C64 version of text files).
The given file is included one line at a time. The original version of COMMENT: will not work with multi line comments in ordinary text files because the file is interpreted/compiled one line at a time, just like text typed in from the keyboard.
Fleet Forth's new INTERPRET allows an elegant solution.
// MULTI LINE COMMENTS
: MLC ( ADR -- )
9 " ;COMMENT" TEXT= 0EXIT
['] (I/C) IS I/C ;
: COMMENT: ( -- )
['] MLC IS I/C ; IMMEDIATE
If the word COMMENT: is encountered, it sets I/C to MLC (Multi Line Comment). MLC checks if the string returned by BL WORD is ';COMMENT' . If it is, MLC sets I/C back to (I/C) .
This version of multi line comments will span multiple lines in a text file. It will even span multiple blocks loaded with THRU . I have also tested it by typing COMMENT: at the Forth's command line. Nothing else typed has any effect until I type ;COMMENT . It even displays the 'OK' prompt if in interpreting state.
I just noticed in the Ans Forth Standard, the word INCLUDE parses the text stream for a file name and the word INCLUDED takes the address and count of a string for the file name.
Fleet Forth's INCLUDE was written to take the address of a counted string.
I've redefined Fleet Forth's INCLUDE so it too parses the text stream.
I still have not made Fleet Forth's INCLUDE nestable and I'm not sure if I will.
Here are words for conditional compilation using Fleet Forth's redesigned interpreter. The code was inspired by the conditional compilation words in the Ans Forth Standard.
The string left on the stack for I/C is a counted string and so is a string compiled by " . TEXT= is used to compare strings. It takes the address of one string, the length to compare, and the address of a second string. It is normally used in MATCH where a sub string is sought in a larger string.
For this application an exact match is sought so that, for example, the string '[THEN]AGAIN' isn't mistaken for the string '[THEN]'. This is why counted strings are used and the size of the comparison for each string is one byte larger than the string. The count byte is treated as part of the string for the comparison. TEXT= doesn't check for valid PETSCII characters. It just compares a certain number of bytes at two addresses for equality.
The conditional compilation words can span multiple blocks or, more importantly, multiple lines in a sequential file (C64 text file).
I'd like to mention something for those new to Forth. With a Forth-83 Standard system, and maybe a few others, it should be possible to experiment with new interpreter ideas without the need to rebuild the system. Just define the new INTERPRET then load the source for QUIT . Here is a fairly generic QUIT .
: QUIT ( -- )
[COMPILE] [
BEGIN
RP! CR QUERY INTERPRET
STATE @ 0=
IF ." OK" THEN
AGAIN ;
Some of you may have noticed that QUIT doesn't set BLK to zero. I've made the assumption that QUERY sets BLK , as well as >IN , to zero like it does in Fleet Forth.
Just type QUIT and the new QUIT loop is running. If there is an ABORT the old quit loop will be running.
Don't forget to type ABORT before forgetting the new words.
Experimenting with a new interpreter can be risky, so caution is advised. This will make Fleet Forth unresponsive.
It will still display the 'OK' prompt. It just won't do anything. This is remedied with the run/stop restore key combination. As a precaution, I added I/C to the table of deferred words which will be reset when a warm start happens. A warm start can be caused by pressing the run/stop and restore keys at the same time. The restore key generates a non-maskable interrupt, so this key combination is also useful to get out of an endless loop.
I have removed the following user variables from Fleet Forth: SPLIM The full mark for the data stack. APLIM The full mark for the auxiliary stack.
When I originally included these variables, the idea was that each task would be able to perform a stack check. This is fine for a system with multiple users; however, Fleet Forth only supports background tasks in addition to the main task. While working with my multitasker, I found that I do not need them. Since background tasks do not run a quit loop, no stack bounds checking is performed. Obviously, code run by a background task needs tested in the main task before it is set to run in the background.
For stack checking in the main task, ?STACK was rewritten so that SPLIM is no longer needed.
A metacompiler macro is a word with a string which is evaluated when the macro is executed. The macros for the Fleet Forth kernel are all defined on screens 2 and 3 of the kernel source.
As for APLIM , the primitives which move data to and from the auxiliary stack perform stack bounds checking. This is necessary since the return stack resides just below the auxiliary stack and above the auxiliary stack are the tables the C64 Kernal uses to keep track of open files.
// SYSTEM CONSTANTS
HEX
85 CONSTANT N 8D CONSTANT XSAVE
8E CONSTANT UP FB CONSTANT IP
FE CONSTANT W
A4F CONSTANT PUSH
A56 CONSTANT NEXT
A81 CONSTANT SETUP
' 2DROP @ CONSTANT POPTWO
PUSH 2+ CONSTANT PUT
POPTWO 2+ CONSTANT POP
' 1 @ 2+ CONSTANT APUSH
APUSH 2+ CONSTANT AYPUSH
AYPUSH 2+ CONSTANT AYPUT
The first five constants are set by design. The value of the others can change depending on how I modify the source for the kernel. Of the constants that are not set by the system design, three where not defined in terms of other constants. As a result, the last step of metacompiling was to insert the system loader disk and load this screen, then issue the metacompiler command ASSEM-CONSTANTS and edit the screen as necessary.
I was able to do something about that and I don't know why it didn't occur to me sooner.
NOOP is a no op defined in Fleet Forth. It is to give some of the deferred words, such as PAUSE , nothing to do. NOOP is a code word with no body. It's code field points directly to NEXT. PUSH is exactly seven bytes before NEXT. CLIT is a code word with a six byte body and the subroutine SETUP follows CLIT in memory.
Here is the modified screen.
Jim, you have quite a few words above that are neither part of any standard I know of, nor defined above. One I'll ask about however is REDEFINE:. It appears to edit the old word to redirect execution to the new one, for secondaries that are already compiled using it, so those secondaries don't need to be recompiled.. Is that what's happening? I've had a way to do that but I like yours more.
I haven't written a word to implement this, but I implemented the idea in my metacompiler. I make use of >FORTH since it is already in Fleet Forth.
And here is a quick implementation of this idea. It does not redirect execution to another name, nor does it create a new name. It just redirects the execution of the old word to a new body. Fleet Forth's END-CODE and ; both need an address to unsmudge and TRUE on the stack. A dummy address is provided since no new name is created.
HERE 0 C, >A
: REDEFINE ( -- ADR TRUE )
HERE ' !
ASSEMBLER [ ASSEMBLER ] MEM
[ A> ] LITERAL TRUE ;
This will redirect execution to a new code word without a name or code field of its own. With >FORTH , the code word can become a high level word. Here is an example.
Notice that in the source for REDEFINE , the context VOCABULARY was set to ASSEMBLER , to include words from the ASSEMBLER vocabulary, and not set back to FORTH ; however, the definition for REDEFINE and the test words were on the same screen and colon sets the CONTEXT vocabulary equal to the CURRENT vocabulary, at least in Fleet Forth and Blazin' Forth.
I recently realized I don't even need to waste a byte of memory as an address for semicolon to 'unsmudge'. Once a code word like TOGGLE (or any of the memory access words) is executing, the W register is not needed until NEXT uses it. Here is a version of REDEFINE which uses the W register. The double slash ( // ) is for end of line comments just like backslash ( \ ).
: REDEFINE ( -- ADR TRUE )
// <NAME>
HERE ' !
ASSEMBLER [ ASSEMBLER ] MEM
W TRUE ;
FORTH
It is used just like the other version.
Here is a version which is not dependent on knowledge of the workings of colon and semicolon, so it is more portable. It does create a new name in the dictionary, but is still used the same.
: (I/C) ( ADR -- )
FIND ?DUP
IF
STATE @ =
IF , EXIT THEN
EXECUTE ?STACK EXIT
THEN
NUMBER? ?HUH ?STACK DPL @ 0<
IF
DROP STATE @ 0EXIT
[COMPILE] LITERAL EXIT
THEN
STATE @ 0EXIT SWAP
[COMPILE] LITERAL
[COMPILE] LITERAL ;
: (I/C) ( ADR -- )
FIND ?DUP
IF
STATE @ 0<> =
IF , EXIT THEN
EXECUTE ?STACK EXIT
THEN
NUMBER? ?HUH ?STACK DPL @ 0<
IF
DROP STATE @ 0EXIT
[COMPILE] LITERAL EXIT
THEN
STATE @ 0EXIT SWAP
[COMPILE] LITERAL
[COMPILE] LITERAL ;
The inclusion of 0<> after STATE @ would allow INTERPRET to work even if STATE has a true value other than -1. Although it adds two bytes and slightly more time ( one extra pass through NEXT ) , it would be more robust. Normally only ] and [ set the value of STATE , so this might be needlessly redundant. Then again, Murphy's law.