Fleet Forth metacompiler redesign

Topics relating to various Forth models on the 6502, 65816, and related microprocessors and microcontrollers.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

JimBoyd wrote:
In solving this problem I came up with two different solutions which resulted in two versions of the metacompiler. They were the same except for what feature was added.
For reference I will refer to the version of the metacompiler without either of these solutions as the base metacompiler. It is this base upon which version A and version B are built.
First, some words in the base metacompiler were cleaned up.
The original source for PNAME and VHEADER .

Code: Select all

: PNAME  ( -- )
   1 VALLOT  1 PNAMES +!
   COLS ?CR ." PADDED: R"
   HERE S? CR ;

: VHEADER  ( >IN -- >IN )
   VNAME TFIND NIP
   ABORT" REDEFINITION IN TARGET"
   HEAD @
   IF
      DUP >IN !
      HERE C@ THERE + 4 +
      SPLIT DROP 0=
      IF  PNAME  THEN
      WLINK VADD
      VNAME C@  THERE $80 VTOGGLE
      VALLOT  THERE $80 VTOGGLE
      1 VALLOT
      EXIT
   THEN
   THERE 1+ SPLIT DROP 0=
   IF  PNAME  THEN
   1 HEADLESS +! ;
HOST

and the improved source.

Code: Select all

: PNAME  ( ADR -- )
   1+ SPLIT DROP ?EXIT      \ If the low byte of the address is not $FF then exit.
   1 VALLOT  1 PADDED +!    \ Pad the virtual header and increment the padded count.
   COLS ?CR ." PADDED: R"   \ If not at left column perform carriage return
   HERE S? CR ;             \ before displaying the name of the padded word.

: VHEADER  ( -- )
   >IN @  VNAME TFIND NIP           \ Save >IN. If the parsed name already exists
   ABORT" REDEFINITION IN TARGET"   \ in the target vocabulary then abort.
   HEAD @                           \ If HEAD is on
   IF                               \ create header.
      >IN !                         \ Restore >IN to parse the name again.
      HERE C@ THERE + 2+ 1+ PNAME   \ Avoid the indirect jump bug.
      WLINK VADD                    \ Create the link field.
      VNAME C@  THERE $80 VTOGGLE   \ Create the name field with high bit set
      VALLOT  THERE $80 VTOGGLE     \ for count and last character.
      1 VALLOT                      \
      EXIT
   THEN                             \ If HEAD is off
   DROP  1 HEADLESS +!              \ increment the count of headerless words
   THERE PNAME ;                    \ and avoid the indirect jump bug.

PNAME now takes an address and pads the header, if necessary, to avoid the code field straddling a page boundary.
VHEADER now no longer requires the value of >IN on the data stack and it returns nothing.

I will present the first solution, metacompiler A.
Here is the base metacompiler's vocabulary structure.

Code: Select all

FORTH
  META
    SHADOW
      FORTH
        ASSEMBLER
  EDITOR
  ASSEMBLER

The vocabulary RUN is added to the META vocabulary.

Code: Select all

FORTH
  META
    SHADOW
      FORTH
        ASSEMBLER
    RUN
  EDITOR
  ASSEMBLER

The variable ALIASES is added to keep a count of how many words have an alias.
The new word ALIAS creates a header in virtual memory and a target word (a handle) in the RUN vocabulary. ALIAS also switches off header creation in virtual memory for the next word with NH .

Code: Select all

: ALIAS  ( ++ )
   ORDER@ [META] RUN DEFINITIONS     \ Save the search order and 
   HEADER NH                         \ create header in RUN vocabulary.
   ORDER!                            \ Restore the search order.
   TRUE HEADLESS +!  1 ALIASES +! ;  \ Correct the headless count and
                                     \ increment the count of aliases.

ALIAS is used like this:

Code: Select all

ALIAS DO
CODE (DO)  ( LIMIT START -- )
   ...
   ...
   END-CODE

Here is the base metacompiler's MCOMPILE .

Code: Select all

CODE MCOMPILE  ( -- )
   >FORTH                           \ Shift to high level Forth.
   ?COMP                            \ Abort if not compiling.
   R> DUP 2+ >R @                   \ Get the CFA of next word in Forth thread containing MCOMPILE
   >NAME COUNT $1F AND >HERE CLIP   \ and use the name field to create a search string at HERE .
   TFIND ?TARGET M, ;               \ Search the TARGET FORTH vocabulary and abort if not found.
                                    \ If found, compile to virtual memory with M, .

and the modified MCOMPILE .

Code: Select all

CODE MCOMPILE  ( -- )
   >FORTH                           \ Shift to high level Forth.
   ?COMP                            \ Abort if not compiling.
   R> DUP 2+ >R @                   \ Get the CFA of next word in Forth thread containing MCOMPILE
   >NAME COUNT $1F AND >HERE CLIP   \ and use the name field to create a search string at HERE .
   TFIND TRUE <>                    \ If not found or if immediate
   IF
      DROP  HERE [META]             \ drop the result and use the string at HERE
      [ ' RUN >BODY ] LITERAL       \ to search the RUN vocabulary.
      VFIND TRUE <>                 \ If still not found
      ABORT" RUN-TIME NOT FOUND"    \ abort.
   THEN
   M, ;                             \ If found, compile to virtual memory.

The modified MCOMPILE for metacompiler A still creates a search string from the name field of the next word in it's definition.

Code: Select all

: TEST
   MCOMPILE (TEST) ; IMMEDIATE

When TEST is executed, MCOMPILE will create the search string "(TEST)" at HERE .
The TARGET FORTH vocabulary is searched by TFIND .If the sought word is not found or if it is immediate, the result is dropped and the RUN vocabulary is searched. Once more, the result has to be for a word which is not immediate.
VFIND does not search parent vocabularies.

This version of the metacompiler allows changing the names of run-time words with ALIAS ; however, there is one downside.
Using DO and it's run-time as an example.

Code: Select all

ALIAS DO
CODE (DO)  ( LIMIT START -- )
   ...
   ...
   END-CODE

There are now two handles on the host system for the same target word in virtual memory, this is not the problem. If the name of the run-time for DO on the host system does not match either one, a new alias is needed.

Code: Select all

ALIAS DO   \ Name of run-time for new kernel. Creates handle DO in RUN vocabulary and header DO in virtual memory.
ALIAS D    \ Name of run-time in host system. Creates handle D in RUN vocabulary. header creation in virtual memory suppressed.
CODE (DO)  \ Name of run-time used in kernel source. Creates handle (DO) in TARGET FORTH vocabulary. header creation in virtual memory suppressed.
   ...
   ...
   END-CODE

This creates three handles for the same word in virtual memory, one in the TARGET FORTH vocabulary and two in the RUN vocabulary.
There is only one header created in virtual memory for this word since ALIAS suppresses header creation in the following word.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »


I will now present the second solution, metacompiler B.
No new metacompiler vocabularies are added. This metacompiler's ALIAS does not create a target word. It creates a header in virtual memory. It does use NH to switch off header creation in virtual memory for the next word.

Code: Select all

: ALIAS  ( -- )
   VHEADER NH
   TRUE HEADLESS +!  1 ALIASES +! ;

ALIAS is used in this metacompiler just like the other version.

Code: Select all

ALIAS DO
CODE (DO)  ( LIMIT START -- )
   ...
   ...
   END-CODE

MCOMPILE was modified slightly so the search only succeeds if the word found is not immediate.

Code: Select all

CODE MCOMPILE  ( -- )
   >FORTH                           \ Shift to high level Forth.
   ?COMP                            \ Abort if not compiling.
   R> DUP 2+ >R @                   \ Get the CFA of next word in Forth thread containing MCOMPILE
   >NAME COUNT $1F AND >HERE CLIP   \ and use the name field to create a search string at HERE .
   TFIND TRUE <>                    \ If not found or if immediate
   ABORT" RUN-TIME NOT FOUND"       \ abort.
   M, ;                             \ If found, compile to virtual memory.

With these modifications it is possible to create a kernel where each of the DO LOOP run-time words has the same name as it's compiling word. The next addition to metacompiler B will allow it to work when built on the new system.
The word :: (double colon) is an alias for the host colon. This will allow accessing colon's functionality while defining words in the META vocabulary, where colon was redefined.

Code: Select all

: ::  ( -- ADR TRUE )
   : ;  // ACCESS TO HOST COLON

These words defined in the META vocabulary are stubs. They don't get compiled or metacompiled. MCOMPILE uses their names to find the corresponding word in the TARGET FORTH vocabulary.

Code: Select all

META DEFINITIONS
:: (DO)   ;
:: (?DO)   ;
:: (LOOP)   ;
:: (+LOOP)   ;
HOST DEFINITIONS

These are metacompiler B's DO LOOP compiling words.

Code: Select all

META DEFINITIONS
:: DO  ( -- >SYSL <SYSL )
   MCOMPILE (DO)
   >MARK 2+ 2+
   <MARK 2+ 2+ ; IMMEDIATE
:: ?DO  ( -- >SYSL <SYSL )
   MCOMPILE (?DO)
   >MARK 2+ 2+
   <MARK 2+ 2+ ; IMMEDIATE
:: LOOP  ( >SYSL <SYSL -- )
   MCOMPILE (LOOP)
   2- 2- <RESOLVE
   2- 2- >RESOLVE ; IMMEDIATE
:: +LOOP  ( >SYSL <SYSL -- )
   MCOMPILE (+LOOP)
   2- 2- <RESOLVE
   2- 2- >RESOLVE ; IMMEDIATE
HOST

Recall that the new semicolon will behave just like the host semicolon when metacompiling is off.
I could have defined these metacompiler DO LOOP compiling words with COMPILE since COMPILE gets patched when metacompiling.
This version of the metacompiler works well; however, there is a slight downside. If I want to rename the run-time for ." so it has the same name as ." , I will need to define a stub for the run-time and a metacompiler version of ." . The same is true for any run-time I wish to rename.

Code: Select all

META DEFINITIONS
:: (.")   ;
:: (ABORT")   ;

:: ."  ( -- )
   COMPILE (.")
   ," ; IMMEDIATE
:: ABORT"  ( -- )
   COMPILE (ABORT")
   ," ; IMMEDIATE
HOST

or

Code: Select all

META DEFINITIONS
:: (.")   ;
:: (ABORT")   ;

:: ."  ( -- )
   MCOMPILE (.")
   V," ; IMMEDIATE
:: ABORT"  ( -- )
   MCOMPILE (ABORT")
   V," ; IMMEDIATE
HOST

I thought of yet another approach to this problem and I think it will yield the best version of the metacompiler.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »


Metacompiler C, the, hopefully, last solution to this problem.
The only differences with metacompiler B:
:: (double colon) is not defined.
MCOMPILE is different.

Code: Select all

: CFA>STR  ( CFA -- HERE )
   >NAME COUNT $1F AND >HERE CLIP ;  \ Use CFA to generate a search string 
                                     \ from a word's name field.
CODE MCOMPILE  ( -- )
   >FORTH
   ?COMP  R> DUP 2+ >R @ CFA>STR     \ Get CFA of next word in definition
                                     \ to generate search string.
   TFIND TRUE <>                     \ If the word is not found or is
   IF                                \ immediate
      DROP  R@ 2- 2- BODY>  CFA>STR  \ Back up to CFA of the word containing
      ASCII ) OVER COUNT + C!        \ MCOMPILE to generate a search string
      DUP DUP 1+ BL MOVE             \ and modify string to be enclosed in
      ASCII ( OVER 1+ C!             \ parenthesis.
      2 OVER +!  TFIND TRUE <>       \ Search the TARGET FORTH vocabulary
      ABORT" RUN-TIME NOT FOUND"     \ and abort if not found.
   THEN
   M, ;                              \ If found, compile to virtual memory.

CFA>STR takes the CFA of a word and generates a string at HERE which is the same as the word's name.
MCOMPILE still generates a search string based on the name of the word to be metacompiled by MCOMPILE . If the search is unsuccessful or the word found is immediate, MCOMPILE will generate a search string from the name of the word which contains METACOMPILE . This search string will be the compiling word's name enclosed in parenthesis.
Using DO as an example.
Suppose the run-time for DO on the host system is still named (DO) .
MCOMPILE generates the string "(DO)" and searches for this string in the TARGET FORTH vocabulary. It will be found and the address for (DO) in virtual memory will be compiled to virtual memory.
Suppose the run-time for DO on the host has a different name. Let's suppose it is something absurd like DO.RUNTIME .
MCOMPILE tries to find DO.RUNTIME in the TARGET FORTH vocabulary and fails.
MCOMPILE then generates a string from the name field of the compiling word, in this case DO . The string will have parenthesis added.
Here is a sample test.

Code: Select all

: TESTWORD
   MCOMPILE FOOBAR ; IMMEDIATE

FOOBAR does not exist in the TARGET FORTH vocabulary. In this example, (TESTWORD) does exist in the TARGET FORTH vocabulary.
MCOMPILE tries to find the target word FOOBAR in the TARGET FORTH vocabulary. When that fails, it tries to find (TESTWORD) in the TARGET FORTH vocabulary and succeeds.
Run-time words with the same name in the host system and the source used by the metacompiler work fine.
Examples are ?BRANCH and BRANCH used by most of the control flow words.
Run-time words with different names in the host system and the source used by the metacompiler will work if the name of the run-time in the source is a parenthesized version of the compiling word's name.
Examples are (DO) compiled by DO and (LOOP) compiled by LOOP . These run-time words can be given a different name on the target being built in virtual memory by using the word ALIAS .

At present, I don't see a downside to this version of the metacompiler
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »


I recently came up with three variations of Fleet Forth's metacompiler. The third variation seemed the best way to go so it is Fleet Forth's metacompiler; indeed, if I'd thought of it first, I would not have bothered with the other two.
As I previously mentioned, the metacompiler patches the word COMPILE to MCOMPILE . This is so I don't have to define a metacompiler version of every word which compiles ?BRANCH or BRANCH , or words which contain such words. I also don't have to define a metacompiler version of some of the other compiling words.
Although the target being built in virtual memory can have redefinitions, a target word (a handle for it's counterpart in virtual memory) in the TARGET FORTH vocabulary can not be a redefinition of another target word.
To allow redefinitions in the target being built, the word ALIAS is used.
There is one run-time word in the kernel which could not be given an alias with the way the latest MCOMPILE was written. One thing to keep in mind, what I mean by this is that any word can be given an alias and the kernel will be metacompiled successfully; however, the Forth system built on the new kernel might not be able to host the metacompiler.
The following deals with using the metacompiler on the new system (I really don't want to maintain two versions of ITC Fleet Forth's kernel).
The run-time word (") , PAREN-QUOTE, is compiled by the word " , QUOTE. The source for QUOTE.

Code: Select all

: "  ( -- )
   STATE @
   IF  COMPILE (") ," EXIT  THEN
   ASCII " CHAR ?MEM  PAD TUCK C!
   TUCK COUNT MOVE ; IMMEDIATE

If compiling, QUOTE compiles PAREN-QUOTE, it's run-time word, and compiles an inline string with ," , COMMA-QUOTE.
If interpreting, the text string in the text stream is copied to PAD and the address of PAD is left on the data stack.

PAREN-QUOTE could not be renamed to QUOTE.
When QUOTE is executed while metacompiling, the name for QUOTE would not be found in the TARGET FORTH vocabulary. I didn't mean a non immediate version couldn't be found, although MCOMPILE checks for that, no version would be found. QUOTE is not even defined in the Forth kernel, it is in the system loader.
MCOMPILE would not be able to back up to the name field of QUOTE to generate a parenthesized version of it's name because COMPILE is not the first word in the body of QUOTE.

The latest improvement to MCOMPILE uses AFIND .
AFIND is a word which takes an address and returns that address and the closest link field address below and the closest link field address above the given address.

Code: Select all

AFIND  ( ADR -- ADR LFA1 LFA2 )

These are excerpts from the source used by the metacompiler.

Code: Select all

ALIAS DO
CODE (DO)  ( LIMIT START -- )
   ...
   END-CODE

   ...

: DO  ( -- >SYSL <SYSL )
   COMPILE (DO)
   >MARK 2+ <MARK 2+ ; IMMEDIATE

The run time of DO is also named DO because of ALIAS .
When DO is executed, MCOMPILE would get the code field of the run-time DO and back up to it's name field. It would generate a search string based on that name and attempt to find a word with the name DO in the TARGET FORTH vocabulary. Failing to do so, it would get the address within the word containing it, DO , and back up to it's name field. MCOMPILE would generate a parenthesized version of the name and use it , (DO) , for the search string.
This is the latest source for MCOMPILE and two helper words.

Code: Select all

: LFA>STR  ( LFA -- HERE )
   L>NAME COUNT $1F AND >HERE CLIP ;
: ?RT  ( FLAG -- )
   ABORT" RUN-TIME NOT FOUND" ;

CODE MCOMPILE  ( -- )
   >FORTH
   ?COMP  R> DUP 2+ >R @          // GET ADR OF NEXT WORD
   AFIND DROP TUCK LINK> = DUP    // DOES IT HAVE A HEADER?
   IF  DROP  LFA>STR TFIND  THEN  // IS IT IN TARGET FORTH
   TRUE <>                        // VOCABULARY?
   IF                             // NOT IN VOC OR HEADERLESS    
      R@ AFIND 2NIP DROP LFA>STR  // GET ADR OF WORD
      ASCII ) OVER COUNT + C!     // CONTAINING MCOMPILE
      DUP DUP 1+ BL MOVE          // USE NFA TO GENERATE
      ASCII ( OVER 1+ C!          // SEARCH STRING AND 
      2 OVER +!  TFIND TRUE <> ?RT  // PARENTHESIZE
   THEN
   M, ;                           // IF FOUND, COMPILE TO VM

LFA>STR takes a word's link field address, converts it to the name field address and copies the word's name to HERE .
?RT aborts if there is a true value on the data stack.
This version of MCOMPILE will work even if the run-time in question has no header on the system the metacompiler is built upon.
MCOMPILE gets the address of the next word in the definition containing MCOMPILE , or COMPILE since COMPILE is patched while metacompiling. AFIND is used to find the closest link field address below this word. This LFA is converted to the CFA and compared with the CFA of the run-time word. If they differ the run-time is headerless.
If it is not headerless MCOMPILE uses it's name for the search string to search the TARGET FORTH vocabulary with TFIND , target find.
If the run-time is not found or it is headerless MCOMPILE uses AFIND to get the name field address of the word which contains MCOMPILE and generate a search string which is a parenthesized version of that name.

With this version of MCOMPILE it does not matter if a run-time word is headerless in the system upon which the metacompiler is built. MCOMPILE will generate a string which is a parenthesized version of the compiling word's name.
It also does not matter if COMPILE or MCOMPILE is not the first word in the compiling words body. Once again AFIND is used to find the word's name field address.

This is an excerpt from the metacompiler source.

Code: Select all

META DEFINITIONS
: (>ASSEM)   ;
: >ASSEM  ( -- )
   ?METACOMP
   ?COMP  MCOMPILE (>ASSEM)
   [ASSEMBLER] MEM ASSEMBLER
   [COMPILE] [ ; IMMEDIATE

The metacompiler has to define it's own version of >ASSEM , even though COMPILE is patched, because it has to set CONTEXT to the metacompiler's ASSEMBLER vocabulary.
Notice the dummy word (>ASSEM) . This stub is defined because the name (>ASSEM) is no longer found in the new system. The run-time for >ASSEM also has the name >ASSEM .
Also notice the metacompiler's version of >ASSEM has MCOMPILE in it's definition.
With the new version of MCOMPILE it is possible to redefine the metacompiler's version of >ASSEM

Code: Select all

META DEFINITIONS
: (>ASSEM)   ;
   ?METACOMP
   ?COMP  MCOMPILE [ 0 , ]
   [ASSEMBLER] MEM ASSEMBLER
   [COMPILE] [ ; IMMEDIATE

MCOMPILE will treat the inline zero as a headerless word and generate the search string (>ASSEM) from the name field of >ASSEM .
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

Take a good look at the disassembly and decompilation of the ABORT" runtime for my Forth. Yes, in my Forth, ABORT" and its runtime have the same name.

Code: Select all

ABORT"
  7740          INX
  7741          INX
  7742   254 ,X LDA  W
  7744   255 ,X ORA  W 1+
  7746  7707 ^^ BEQ ' " >BODY 4 +
  7748  3378    JSR ' >FORTH >BODY
  7751  7921 WHERE
  7753  6778 CR
  7755  4789 R@
  7757  7169 S?
  7759  8742 ABORT
21 

In the high level portion of the decompilation, the first number on a line of decompilation is the address in the decompiled word. The second number is the CFA of the word in the definition of the decompiled word.

Code: Select all

   --------------- address within the body of ABORT" runtime
   |     --------- Code field address of ABORT
   V     V
  7759  8742 ABORT

Notice in this fragment two of the words used by the runtime are defined after the ABORT" runtime.

Code: Select all

  7751  7921 WHERE  \ Defined after ABORT" runtime.
  7753  6778 CR
  7755  4789 R@
  7757  7169 S?
  7759  8742 ABORT  \ Defined after ABORT" runtime.

With my Forth's older version of INTERPRET , the ABORT" runtime was needed by INTERPRET to ABORT" if a string which was not found in the dictionary could not be converted to a number. INTERPRET was needed by QUIT which was needed by ABORT which was needed by the ABORT" runtime.
Looking at this fragment and noting that WHERE and ABORT are defined after the ABORT" runtime might lead one to believe this Forth was assembled. This is not the case. It was metacompiled. Here is the source for the ABORT" primitive.

Code: Select all

ALIAS ABORT"
CODE (ABORT")  ( F -- )
   INX  INX
   $FE ,X LDA  $FF ,X ORA
   ' (") >BODY 4 +  0= BRAN
   >FORTH
   LABEL DO.WHERE NOOP
   CR R@ S?
   LABEL DO.ABORT NOOP -;

And the high level portion.

Code: Select all

   LABEL DO.WHERE NOOP
   CR R@ S?
   LABEL DO.ABORT NOOP -;

Each label holds the value of THERE , the target system's HERE , when it is created. The word NOOP is used as a placeholder.
The following shows the resolution of the address at label DO.ABORT

Code: Select all

: ABORT  ( -- )
   ERR SP! AP! QUIT -;
' ABORT DO.ABORT !

The resolution for DO.WHERE is similar.
That is how to handle forward references in the source used by the metacompiler without resorting to creating a deferred word in the target for something which should not change.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

My Forth, an ITC Forth, has a few words with no body and a code field which points into the body of a primitive. The definition of these words follows this pattern:

Code: Select all

CODE <NAME>
   <ADDRESS>  LATEST NAME> !
   END-CODE

Here is an example:

Code: Select all

CODE ON  ( ADR -- )
   DEY  TYA  DEX  DEX
   ' SWAP! @ 4 + 0= NOT BRAN
   END-CODE
CODE OFF  ( ADR -- )
   ' ON @ 1+ LATEST NAME> !
   END-CODE

This metacompiler technique does not work if the word is to be headerless. Although there is a word with a header on the host to serve as a handle, a headerless word has no header in the target, which is being built in virtual memory; therefore, the metacompiler versions of LATEST and NAME> do not work.
Since the metacompiler version of ' (tick) does not require the presence of a header in the target, the following technique works.

Code: Select all

NH
CODE <NAME>
   END-CODE
   <ADDRESS>  ' <NAME> !

In the previous example, OFF could be defined like this:

Code: Select all

CODE OFF  ( ADR -- )
   END-CODE
   ' ON @ 1+ ' OFF !

JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

The creation of Fleet Forth's metacompiler's assembler vocabulary lexicon takes advantage of a property of Fleet Forth vocabularies. Although Fleet Forth's vocabularies have a branching structure just as FIG Forth vocabularies, there is no false header embedded in a Fleet Forth vocabulary. A Fleet Forth vocabulary has a body with three cells. The first cell points to the link field of the latest word defined in that vocabulary, the second cell points to the parent vocabulary and the third cell is part of the chain of all vocabularies pointed to by VOC-LINK .
Normally, the first word in a Fleet Forth vocabulary has a link field with the value Zero. I say "normally" because the metacompiler assembler violates this rule. The first word defined in the metacompiler vocabulary has a link field which points to the word NOT in Fleet Forth's main assembler vocabulary. This allows the words defined in the Fleet Forth main assembler vocabulary between NOT and the first word in that vocabulary to be searched when the metacompiler assembler vocabulary is searched. This was done to save memory since these words are, or would have been, defined exactly the same in the metacompiler assembler vocabulary. Since Fleet Forth vocabularies have a parent link rather than a false header (the parent link in the main Forth vocabulary is, obviously, zero), the search order when searching starts with the metacompiler assembler is still: metacompiler assembler, target Forth, shadow vocabulary, meta vocabulary and then the main Forth vocabulary.

Code: Select all

TARGET ASSEMBLER ORDER
CONTEXT: ASSEMBLER
         FORTH
         SHADOW
         META
         FORTH

CURRENT: FORTH
         SHADOW
         META
         FORTH
 OK

I will keep the metacompiler assembler vocabulary like this on Fleet Forth; however, it is my desire that others will implement a metacompiler on their own Forths. To that end, I suggest the following change for all Forths which do not have VOCABULARY defined similarly to Fleet Forth's.
Remove the following from the source for the metacompiler vocabulary lexicon.

Code: Select all

\ TIE IN WITH FLEET FORTH ASSEMBLER
TARGET VOCABULARY ASSEMBLER
HOST ASSEMBLER ' NOT >LINK
TARGET ' ASSEMBLER >BODY !
HOST
: [ASSEMBLER]   [TARGET] ASSEMBLER ;
   IMMEDIATE
TARGET ASSEMBLER DEFINITIONS

And replace it with the following source.

Code: Select all

TARGET VOCABULARY ASSEMBLER
HOST
: [ASSEMBLER]   [TARGET] ASSEMBLER ;
   IMMEDIATE
TARGET ASSEMBLER DEFINITIONS

\ OPCODE OFFSET TABLE AND MODES
CREATE TABLE
   $0008 , $0810 , $1404 , $2C14 ,
   $1C0C , $0818 , $1000 , $0400 ,
   $1414 , $0C2C , $1C1C ,
VARIABLE MODE
: AM  ( N -- )
   CREATE  ( N -- )
      C,
   DOES>   ( -- )
      C@ MODE ! ;
0 AM .A   1 AM X)   2 AM )Y
3 AM #    7 AM )    8 AM MEM
9 AM ,X  $A AM ,Y

\ Z
: Z  ( OA? ADR -- OA? BOP BMAP )
   COUNT SWAP @
   MODE @ 7 >        0EXIT
   2PICK $100 U<     0EXIT
   1 MODE @ 2- 2-
   LSHIFT  OVER AND  0EXIT
   -4 MODE +!        ;

\ ?EXEC BO BRANCHES NOT
FORTH DEFINITIONS
: ?EXEC  ( -- )  STATE @
   ABORT" FOR ASSEMBLING" ;
ASSEMBLER DEFINITIONS
: BO  ( DEST SRC -- BRANCH.OFFSET )
   1+ - DUP 0< OVER ABS +  $7F >
   ABORT" BRANCH RANGE EXCEEDED" ;
$50 CONSTANT VS   \ OVERFLOW
$90 CONSTANT CS   \ CARRY SET
$D0 CONSTANT 0=   \ EQUAL TO ZERO
$10 CONSTANT 0<   \ LESS THAN ZERO
$90 CONSTANT >=   \ NOT LESS THAN
\ >= ONLY CORRECT AFTER SUB, OR CMP
: NOT  ( CC1 -- CC2 )
   BL XOR ;         \ REVERSE TEST

It is my hope that others will explore this fascinating method for building new Forth kernels. Metacompiling is my preferred method for building a Forth kernel because it allows me to more easily improve the kernel.
When I change the definition for a kernel word, I have a fully functional kernel I can test it with ( partly because my Forth allows redefinitions). Since I work directly with the source used by the metacompiler, once testing is done and the new version of the kernel word works as expected, the source for said word is metacompiler ready. If I was using an assembler to build my Forth kernel and I wanted to improve one of the kernel words, I would have to write that word in a form suitable for my Forth system. Once the kernel word was working, I would have to translate the source to a form suitable for the assembler used to build the kernel and hope the translation process didn't introduce any errors from typos or other mistakes.

Yes, this is a hobbyist metacompiler. In my opinion it is better than a commercial "black box" metacompiler for two reasons, it is transparent and it leaves the Forth system it is built on exposed. If the metacompiler misbehaves, it is possible to troubleshoot it. If it lacks a feature, it is possible to add it, within reason.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

The word to switch metacompiling on, META.ON , patches some of the kernel words; therefore, metacompiling must be switched off with META.OFF before the metacompiler lexicon, and its associated vocabularies, is forgotten.

Code: Select all

\ PATCH KEY FORTH WORDS
' (I/C)
" 2+ DUP @ ' , = >IN !"
COUNT EVALUATE >A
: META.ON  ( -- )
   ['] MFIND
   [ ' (I/C) >BODY ] LITERAL !
   ['] M,  [ A@ ] LITERAL !
   ['] MLITERAL @ ['] LITERAL !
   ['] >MMARK @ ['] >MARK !
   ['] <MMARK @ ['] <MARK !
   ['] >MRESOLVE @ ['] >RESOLVE !
   ['] <MRESOLVE @ ['] <RESOLVE !
   ['] MCOMPILE @ ['] COMPILE !
   ['] V," @ ['] ," ! ;

\ RESTORE KEY FORTH WORDS
: META.OFF  ( -- )
   ['] FIND
   [ ' (I/C) >BODY ] LITERAL !
   ['] ,  [ A> ] LITERAL !
   [ ' : @ ] LITERAL
   DUP ['] LITERAL !
   DUP ['] >MARK !
   DUP ['] <MARK !
   DUP ['] >RESOLVE !
   DUP ['] <RESOLVE !
   DUP ['] COMPILE !
       ['] ," ! ;

META.OFF is included in the definition of FINISH and FINISH is included in the definition of FINISH-FORTH ; however, it is possible to work with the system with metacompiling on to get a better feel for what the metacompiler is doing or work on enhancements to the metacompiler. It can be all to easy to neglect switching metacompiling off with META.OFF before removing the metacompiler lexicon by forgetting the first of the metacompiler words.
One simple redefinition eliminates this potential hazard.

Code: Select all

: FORGET  ( ++ )
   META.OFF FORGET ;

The redefinition of FORGET should be defined immediately after META.OFF and defined in the host FORTH vocabulary ( the original FORTH vocabulary) just like META.OFF .
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

Those who prefer to reset the dictionary to its current "empty" point with the Forth word EMPTY should also include a redefinition of EMPTY in the metacompiler source along with the redefinition of FORGET .

Code: Select all

: EMPTY
   META.OFF  EMPTY ;

I realise that the redefinitions return to high level Forth which is no longer a part of the dictionary to execute EXIT at the end of the redefinition; however, Fleet Forth's FORGET does not wipe the memory between the new HERE and the original. Nor does it execute any words which use memory at HERE or PAD . FORGET just resets the dictionary pointer after resetting the pointers in each VOCABULARY . Since EMTPY branches into the body of FORGET (to handle the "heavy lifting"), it also does not affect memory above the new HERE address. The code for these words still exist in memory until something alters that memory AFTER the new FORGET or EMPTY finish executing and return control.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

I've mentioned in the thread "Fleet Forth design considerations" that Fleet Forth's CREATE will add a pad byte before any fields of a word are compiled if the code field or any of the following four cells will straddle a page boundary. This allows Fleet Forth to have multi code field words with up to five code fields, even though it is for a Commodore 64 and the 6510 processor with its indirect jump bug.
To add multi code field VALUE words to the kernel, it was necessary to modify the metacompiler so the first few cells after the code field also avoid straddling a page boundary.
One word in the metacompiler source needed changed. The word PNAME is passed the address where the code field will land. If the code field will straddle a page boundary, PNAME allots a byte, increments the variable PADDED and displays the name of the word which will be padded.

Code: Select all

: PNAME  ( ADR -- )
   1+ SPLIT DROP ?EXIT
   1 VALLOT  1 PADDED +!
   COLS ?CR ." PADDED: {reverse}"
   HERE S? CR ;

Where the source shows "{reverse}" the Commodore and R keys are pressed. This places the Commodore in reverse video mode.
The new version of PNAME which keeps the code field and the next four cells from straddling a page boundary:

Code: Select all

: PNAME  ( ADR -- )
   DUP 9 + SPLIT DROP
   9 U< AND  2 MOD 0EXIT
   1 VALLOT  1 PADDED +!
   COLS ?CR ." PADDED: {reverse}"
   HERE S? CR ;

JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

JimBoyd wrote:
My Forth system has three VALUE words in the kernel.
DO.VALUE and TO.VALUE are built without headers or code fields. They have labels. The system DO.VARIABLE also has the label DO.VARIABLE . The three VALUE words are built like this.

Code: Select all

4 VALUE #BUF           4 ALLOT


3 , 0 ,
HSUBR DO.VALUE  ( -- N )
   6 # LDY
   ' CONSTANT >BODY 8 + JMP
END-CODE

HSUBR TO.VALUE  ( N -- )
   4 # LDY
   0 ,X LDA  W )Y STA  INY
   1 ,X LDA  W )Y STA
   POP JMP
END-CODE

DO.VALUE  ' #BUF !
TO.VALUE  ' #BUF 2+ !
DO.VARIABLE  ' #BUF 4 + !
4  ' #BUF 6 + !

Defining VALUE words in this way in the kernel seemed a bit clumsy. The following changes were made to the Fleet Forth kernel source to make it easier.
An empty block was added just after the load block and the load block was modified.
The original load block

Code: Select all

// LOAD BLOCK FOR FLEET FORTH
START
  1 FH #164 FH THRU
HEX
FINISH-FORTH

Code: Select all

and the new.
// LOAD BLOCK FOR FLEET FORTH
DECIMAL
  1 FH #164 FH THRU
HEX
FINISH-FORTH

START was removed, which means the very next block which is loaded is loaded with metacompiling off.
Note: Fleet Forth VARIABLEs and 2VARIABLEs are created with their parameter fields set to all zeros.

Code: Select all

// METACOMPILER VALUE
HOST
2VARIABLE VALUE.CF1
2VARIABLE VALUE.CF2
2VARIABLE VALUE.CF3
META DEFINITIONS FORTH
: VALUE  ( N -- )
   [HOST]
   >IN @ HEADER >IN !
   VALUE.CF1 CF,
   VALUE.CF2 CF,
   VALUE.CF3 CF,
   DUP V,
   >SHADOW CONSTANT ;

START is added to the top of the very next block to initiate metacompiling.
This definition of VALUE is a metacompiler word used to build the VALUE words in the target kernel being built in virtual memory.
Another change was made to the metacompiler. The word UNCHAIN was factored out of PATCH-CF

Code: Select all

// UNCHAIN PATCH-CF
: UNCHAIN  ( ADR -- )
   THERE OVER !  2+  DUP @ SWAP OFF
   BEGIN
      ?DUP  0EXIT
      DUP  V@  THERE ROT V!
   AGAIN -;
: PATCH-CF  ( -- )
   LATEST COUNT $1F AND >HERE CLIP
   ['] META >BODY VFIND
   0= ABORT" DEFINER MISSING"
   DUP @ DEFINER-WORD <>
   ABORT" NOT A DEFINER WORD"
   >BODY 2+ UNCHAIN ;

and used in the block where the code fields for VALUE are defined.

Code: Select all

// DO.VALUE TO.VALUE
3 , 0 ,
HSUBR DO.VALUE  ( -- N )
   VALUE.CF1 UNCHAIN
   6 # LDY
   ' CONSTANT >BODY 8 + JMP
END-CODE
HSUBR TO.VALUE  ( N -- )
   VALUE.CF2 UNCHAIN
   4 # LDY
   0 ,X LDA  W )Y STA  INY
   1 ,X LDA  W )Y STA
   POP JMP
END-CODE

UNCHAIN is also used in the last block which has the definition of the kernel's CREATE . CREATE itself is unaltered. The third code field of a VALUE word points to DO.VARIABLE

Code: Select all

       .
       .
       .
       
   ;CODE
   LABEL DO.VARIABLE
   VALUE.CF3 UNCHAIN
   CLC
   W    LDA  2 # ADC
   W 1+ LDY  CS IF  INY  THEN
   AYPUSH JMP  END-CODE

With these changes, the '4 ALLOT' after the definitions of the three VALUE words in the kernel was removed and the need to manually set each code field and parameter field was removed.
This was no longer needed:

Code: Select all

DO.VALUE  ' #BUF !
TO.VALUE  ' #BUF 2+ !
DO.VARIABLE  ' #BUF 4 + !
4  ' #BUF 6 + !

For any VALUE word defined in the kernel.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »


In the source for Fleet Forth, there are two places where metacompiling is momentarily switched off.

Code: Select all

$334
META.OFF TO USER.AREA META.ON

and

Code: Select all

HERE
META.OFF TO 'THERE META.ON

Anytime I looked at that, it looked clumsy and I thought I could do better.
For USER.AREA In the metacompiler's source for <MUSER> ,I replaced this:

Code: Select all

      C@ USER.AREA + ;

with this:

Code: Select all

      C@  " USER.AREA"  [SHADOW]
      ['] SHADOW >BODY VFIND
      HOST FORTH
      0= ABORT" NO USER AREA"
      >BODY @ + ;

For 'THERE I first changed it to EMPTY.POINT . The new name is meant to convey the idea that the memory location it references holds the address the dictionary will be pruned back to by EMPTY . I use the word 'prune' because EMPTY performs a proper pruning of the dictionary by setting FENCE equal to the empty point and branching into FORGET with that address on the data stack.

the other change was in PATCH-FORTH . This:

Code: Select all

   THERE 'THERE V!

was replaced with this:

Code: Select all

   THERE  " EMPTY.POINT"  [SHADOW]
   ['] SHADOW >BODY VFIND HOST FORTH
   0= ABORT" NO EMPTY POINT"
   >BODY @ V!

Sure, it's a little more complex; however, this was eliminated:

Code: Select all

0 VALUE USER.AREA
0 VALUE 'THERE

The following was removed from META-STATUS

Code: Select all

   CR ."    'THERE: " 'THERE U.W
   CR ." USER AREA: " USER.AREA U.W

The following was removed from START

Code: Select all

   0 TO 'THERE
   0 TO USER.AREA

and the following was removed from FINISH-FORTH

Code: Select all

   'THERE 0= ABORT" 'THERE IS ZERO"
   USER.AREA 0= ABORT" NO USER AREA"

so the overall complexity may be approximately the same.
The idea is <MUSER> searches the dictionary for the word USER.AREA and fetches the data in its parameter field. Similarly, PATCH-FORTH searches the dictionary for EMPTY.POINT and fetches the data from its parameter field.

The nice thing about this approach is it eliminates those clumsy looking sections in the Fleet Forth source.

Code: Select all

$334
META.OFF TO USER.AREA META.ON

was replaced with

Code: Select all

$334 DEFINE USER.AREA

and

Code: Select all

HERE
META.OFF TO 'THERE META.ON

was replaced with

Code: Select all

LABEL EMPTY.POINT

A much nicer read in the Fleet Forth source.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

JimBoyd wrote:
I've already mentioned elsewhere that using labels within a definition is not a problem. The label is defined in the host dictionary while the target is built in virtual memory. This is why labels can be defined as needed.
When testing a kernel word on the host system, labels can not be created in the middle of a word. Here is a version of LABEL which does not create a new word. It resides in the host Forth vocabulary and requires a label to be a VALUE which has already been defined.

Code: Select all

: LABEL  ( -- )
   ORDER@ FORTH // HOST FORTH VOCABULARY
   HERE ' DUP @
   [ ' #BUF @ ] LITERAL <>
   ABORT" NOT A LABEL"
   >BODY !  ORDER! ; IMMEDIATE

Both versions of LABEL exist in the metacompiler. The first version is defined in the META vocabulary and is found when metacompiling. The second is defined in the host FORTH vocabulary and is found when loading a kernel word into the host for testing.

The metacompiler no longer has two versions of LABEL . The HOST version was removed.
A new version of LABEL can be loaded either with the metacompiler or independent of it for testing words in Fleet Forth's kernel source.
This new non metacompiler version of LABEL does not require the existence of predefined VALUEs, it is built on the shadow words described in the thread on shadow words in Forth.

Code: Select all

VARIABLE SHADOWLANDS
MRU 5 1024 * - SHADOWLANDS !
: SWITCH  ( -- )
   DP @ SHADOWLANDS @
   DP ! SHADOWLANDS ! ;
: DEFINE  ( N -- )
          ( -- N )
   SWITCH CONSTANT SWITCH ;
: LABEL  ( -- )
         ( -- ADR )
   HERE DEFINE ; IMMEDIATE
: HSUBR  ( -- )
   [COMPILE] LABEL [ ASSEMBLER ]
   W TRUE ASSEMBLER MEM ;
: NH   ;
: ALIAS  ( ++ )
   BL WORD DROP ;

MRU is the start of Fleet Forth's block buffer table. The line

Code: Select all

MRU 5 1024 * - SHADOWLANDS !

places the start of the shadowlands five kilobytes from the block buffer table.
In this usage, PRUNE isn't even needed as the words being tested will also be deleted when testing is done; although, it doesn't hurt to have it.

Code: Select all

: PRUNE  ( -- )
   CURRENT @
   BEGIN
      DUP HERE TRIM
      @ DUP [ HERE ] LITERAL U<
   UNTIL
   DROP ;

With the shadow words, I can load something like this:

Code: Select all

CODE I  ( -- LOOP-INDEX )
   XSAVE STX  TSX  CLC
   $101 ,X LDA  $103 ,X ADC  TAY
   $102 ,X LDA  $104 ,X ADC
   XSAVE LDX
   LABEL  YAPUSH
   DEX  DEX
   0 ,X STY  1 ,X STA
   NEXT JMP  END-CODE

because the LABEL YAPUSH is built in higher memory, not in the middle of the DO LOOP index word I .
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »


I recently had an idea for state smart DEFINE words. Here is the source for the metacompiler's state smart DEFINE and LABEL , which is based on DEFINE .

Code: Select all

META DEFINITIONS
: DEFINE  ( W -- )
   >SHADOW
   CREATE IMMEDIATE
      ,
   DOES>
      @  STATE @
      IF
         [COMPILE] LITERAL
      THEN ; IMMEDIATE
: LABEL  ( -- )
   HERE THERE [COMPILE] DEFINE
   HERE SWAP - CDP +! ; IMMEDIATE
HOST

The word >SHADOW has nothing to do with shadow words. The metacompiler lexicon adds four new vocabularies to Fleet Forth. One vocabulary is SHADOW . The metacompiler builds its target in virtual memory; therefore, nothing in the target is executable. Some variables, such as STATE and SPAN , need interpreted while assembling code words. The SHADOW vocabulary has constants with the same names as the target variables to return the correct addresses in the target. There are also shadow constants for the constants in the target image.

With state smart versions of these words, I was able to remove MACRO from the metacompiler source. As useful as macros can be, they were only used for the evaluation of numerical strings.

Code: Select all

: (MACRO)  ( ADR CNT -- )
   CREATE IMMEDIATE
   >HERE C@ 1+ ALLOT
   [ HERE >A ]
   DOES>
   RP@ $11F <
   ABORT" RECURSIVE ERROR"
   COUNT EVALUATE ;
A> 2+ CONSTANT MACRO-CF
META DEFINITIONS
: MACRO  ( ADR CNT -- )
   >SHADOW (MACRO) ;
HOST

Following this change to the metacompiler, I removed the macros from the source for Fleet Forth.
Source like this:

Code: Select all

" $98"  COUNT MACRO #OPEN.FILES
" $99"  COUNT MACRO INPUT
" $C6"  COUNT MACRO #KBD
" $C7"  COUNT MACRO REV
" $CC"  COUNT MACRO BLINK.ENABLE
" $CD"  COUNT MACRO BLINK.TIMER
" $CF"  COUNT MACRO BLINK.ON?
" $D3"  COUNT MACRO CURSOR.COL
" $D4"  COUNT MACRO QUOTE.MODE
" $258" COUNT MACRO LFN-1
" $277" COUNT MACRO KBD.BUFFER
" $292" COUNT MACRO AUTO.SCROLL.DN

was changed to this:

Code: Select all

$98  DEFINE #OPEN.FILES
$99  DEFINE INPUT
$C6  DEFINE #KBD
$C7  DEFINE REV
$CC  DEFINE BLINK.ENABLE
$CD  DEFINE BLINK.TIMER
$CF  DEFINE BLINK.ON?
$D3  DEFINE CURSOR.COL
$D4  DEFINE QUOTE.MODE
$258 DEFINE LFN-1
$277 DEFINE KBD.BUFFER
$292 DEFINE AUTO.SCROLL.DN

Not only was the source for the metacompiler simplified ( even if only slightly), there is now one less word needed for the test suite to test changes to a word, or group of words, in Fleet Forth's kernel before building a new kernel.
JimBoyd
Posts: 931
Joined: 05 May 2017

Re: Fleet Forth metacompiler redesign

Post by JimBoyd »

I mentioned that Fleet Forth's LATEST is a system value set by CREATE . What this means is LATEST is created by the metacompiler as a CONSTANT ; therefore, a constant with the name LATEST is defined in the SHADOW vocabulary at that time. This made it impossible to use LATEST while meta interpreting because the constant defined in the SHADOW vocabulary would be found before any version of LATEST in the META vocabulary, at least until I rewrote the metacompiler yet again and changed the order in which the metacompiler vocabularies are created.
The SHADOW vocabulary is created first.
The META vocabulary is created next.
Then the target FORTH vocabulary.
Finally the metacompiler's ASSEMBLER vocabulary.
Now when meta compiling or meta interpreting, the search order starts with the metacompiler's ASSEMBLER if target assembling, to the target FORTH vocabulary, then the META vocabulary, the SHADOW vocabulary, followed by the host FORTH vocabulary.

Code: Select all

VOCS 
FORTH
  SHADOW
    META
      FORTH
        ASSEMBLER
  EDITOR
  ASSEMBLER
 OK

Post Reply