Link here

Chapter 16 - Advanced Topics


This chapter discusses the implementation of the QED-Forth interpreter and compiler, vocabularies, recursion, deferred compilation, synonym creation, forward references, memory expansion, page changing, and subroutine threaded code.

 

The QED-Forth Interpreter

This section presents a "top down" look at the QED-Forth interpreter and compiler. The highest level words are discussed first, and then the constituent words are described.

 

ABORT

ABORT is executed at the end of every restart and error recovery sequence. Its action can be customized by the programmer. Its equivalent definition is:

: ABORT    ( -- )
    CUSTOM.ABORT @
    IF      UABORT X@ EXECUTE
    ELSE    (ABORT)
    ENDIF
;

If CUSTOM.ABORT is true, ABORT fetches the user-supplied xcfa from UABORT and executes the specified routine. If CUSTOM.ABORT is false, ABORT executes the default action named (ABORT). The default abort action clears the data and return stacks, executes FORTH DEFINITIONS so that FORTH is both the CURRENT and CONTEXT vocabulary, and then checks for an autostart vector installed by the PRIORITY.AUTOSTART routine. If such a startup word is found, it is executed. If it is not found, or if the specified startup word returns (via an RTS or ;), then the abort routine checks for an autostart vector installed by AUTOSTART in EEPROM. If such a startup word is found, it is executed. If it is not found, or if the specified startup word returns (via an RTS or ;), QUIT is called. QUIT is the QED-Forth interpreter that interprets, compiles and executes commands from the input stream.

The equivalent high level definition of (ABORT) is

HEX
: (ABORT)    ( -- )
    0 (PAGE.LATCH) (C!)            \ set default page
    RP!   SP!                 \ clear return and data stacks
    *RESTORE.LOCALS.LATEST            \ clean up if locals were compiling
    FORTH  DEFINITIONS             \ use FORTH vocabulary
    *PRIORITY.STARTUP.VECTOR @ 1357 =    \ valid priority autostart pattern?
    IF                        \ if so, call the specified autostart routine
        *PRIORITY.STARTUP.VECTOR X@    ( xcfa -- )
        EXECUTE                ( -- )
    ENDIF                \ if no priority autostart, or if routine returned,
    *STARTUP.VECTOR @ 1357 =            \ valid autostart pattern in EEPROM?
    IF                        \ if so, call the specified autostart routine
        *STARTUP.VECTOR X@            ( xcfa -- )
        EXECUTE                ( -- )
    ENDIF                \ if no autostart, or if routine returned,
    QUIT                    \ call QUIT to enter interpreter
;

The words that start with the * character are headerless and cannot be found in the dictionary.

 

Forth Interpreter

QUIT is the QED-Forth interpreter. It is an infinite loop that continually interprets the next line or block from the input stream. An equivalent high level definition is:

: QUIT        ( -- )    \ this is the top level FORTH word
    [             \ enter execution mode
    *LP!        \ clear locals stack, clear locals.are.compiling
                \ and clear trace indentation flag
    BEGIN    \ enter an infinite loop
        STATE @ 0=
        IF   RP!      \ if we're executing, clear the return stack
        ENDIF
        *?STACK    \ clear the data stack if there is underflow
        CR    \ issue carriage return and line feed
        QUERY    \ read next line into TIB, init >IN = 0, set #TIB
        INTERPRET    \ move each word to pocket and interpret it
        *OK?    \ if we're executing, print " ok" and,
            \ if debug is true, print the stack contents
AGAIN        \ this is an infinite loop,terminated by errors
;

The definition of *OK? is:

: *OK?    ( -- )
    STATE @ NOT
    IF            \ if we're executing, print ok
        SPACE ." ok"
        DEBUG @
        IF    .S      \ if executing and debug flag set, print stack
        ENDIF
    ENDIF
;

Note that the interpreter functions by repeatedly calling QUERY to read in the next line of input from the serial line, and INTERPRET to interpret the commands on the line. Let's look at these words in more detail.

 

QUERY

QUERY calls the main serial input routine EXPECT. The word

EXPECT ( xaddr\n – )

inputs up to n input characters from the serial input port and stores them in a buffer starting at the specified xaddr. EXPECT terminates after n characters or a carriage return is received, whichever occurs first. The low level I/O words KEY and EMIT receive and echo the characters; these routines call the PAUSE task switch word as explained in the "Multitasking" chapter. EXPECT sets the system variable SPAN to the total number of characters received (excluding the final carriage return, if present). The carriage return is echoed as a blank character. Each tab character (ascii 9) is converted to spaces (the number of spaces is specified by the user variable TAB.WIDTH whose default value is 4. Incoming null characters (ascii 0) and linefeeds (ascii 10) are ignored. Incoming XON and XOFF characters (ascii 17 and 19, respectively) are not echoed or stored, but they clear and set the XMIT.DISABLE flag in the system user area. This may be useful for programmers who wish to establish a serial handshaking protocol between QED-Forth and a remote computer.

The definition of QUERY is

: QUERY    ( -- )
    TIB CHARS/LINE @  EXPECT
    >IN OFF   BLK OFF
    SPAN @  #TIB !
;

QUERY specifies the TIB (terminal input buffer) as the location where the input characters are to be stored, and specifies a maximum of CHARS/LINE characters per line (CHARS/LINE is a user variable). After EXPECT executes, QUERY gets ready for interpretation by initializing >IN to 0 (>IN is the offset that points to the current position in the TIB), and setting BLK to 0 to indicate that the input stream is the serial port as opposed to mass memory. The contents of SPAN are transferred to #TIB to save the number of characters actually received.

 

INTERPRET

The word INTERPRET acts as its name suggests: it interprets a line or a block of input commands. Its equivalent definition is

: INTERPRET    ( -- )
BEGIN
    BL WORD    ( -- xaddr | = POCKET )  \ get next word to POCKET
    DROP DUP (C@)    \ input is present if count is non-zero
WHILE        \ interpret only if input is present...
    *(INTERPRET)    \ interpret the word at POCKET
REPEAT
DROP         \ drop POCKET
;

INTERPRET calls BL WORD to parse the next blank-delimited word from the input stream. WORD (described below) checks the contents of BLK to see if the input stream is the TIB or a block from mass memory, and moves the word to POCKET. If the input stream is exhausted, WORD leaves a count of zero at POCKET. While input is present, *(INTERPRET) is called to interpret the next word of input which is at POCKET. INTERPRET finishes when the input stream is exhausted. This occurs after the last word on a line or in a block. When INTERPRET finishes, QUIT calls QUERY to get the next line of input and the process starts over.

Most of the interpretation is performed by the headerless subsidiary routine *(INTERPRET). This routine interprets the next word from the input stream which WORD has left in the buffer at POCKET. Its equivalent high level definition is:

: *(INTERPRET)    ( $addr -- )   \ $addr = pocket in common memory
\ interpret the next single word in the input stream
(#FIND)        ( --[xcfa\xnfa\ +/-1 ] or [0]) \ +1 if immed.
?DUP IF        \ if word was found...
    STATE @ =
    IF    *TRACE?    ( -- xcfa ) \ compile trace if TRACE is on
        COMPILE.CALL    \ if not immediate AND we're compiling, compile it
    ELSE    *TRACE?    \ compile trace if TRACE is on AND it's assm command
        EXECUTE     \ execute it if it's immediate or we're in executing
    ENDIF    ( -- )
ELSE            \ if word wasn't found, try to convert it to a number
    POCKET NUMBER    ( -- [n\1] or [d\2] or [0] )
    ?DUP IF    \ if it's a valid integer,
        1- NDROP     ( -- n ) \ only use lowest 16bits of number
        *TRACE.LIT    \ compile trace if TRACE is on AND we're compiling
        [COMPILE] LITERAL    \ leave on stack or compile as literal
    ELSE        \ if not a valid integer try to convert to floating point
        POCKET FNUMBER    ( -- [r\-1] or [0] ) \ see if it's a fp#
        0= IF    \ if not a fp#,it's an invalid input
            *UNRECOGNIZED.INPUT.ERROR     \ error: unrecognized input
        ENDIF
        *TRACE.2LIT    \ compile trace if TRACE on AND we're compiling
        [COMPILE] 2LITERAL    \ leave on stack or compile fp literal
    ENDIF
ENDIF
*?STACK        \ check for data stack underflow
;

*(INTERPRET) calls (#FIND) to try to find the word's name (which is at POCKET) in the dictionary. (#FIND) is explained below. If the word is found, the extended code field and name field addresses are left on the stack under a flag that is +1 if the word is immediate and -1 if not. If the found word is immediate, or if QED-Forth is in execution mode, the word is executed. If the word is not immediate and Forth is in compilation mode, the word is compiled by invoking COMPILE.CALL. *TRACE? removes the extended name field address from the stack, checks the TRACE flag and compiles a trace instruction if necessary.

If the word is not found, *(INTERPRET) tries to convert it to an integer. If the string at POCKET is converted to an integer, it is left on the stack (if executing) or compiled (if compiling) as a 16-bit literal. (Recall that 32-bit double numbers must be entered with the DIN command.) If the string at POCKET cannot be converted to an integer, *(INTERPRET) tries to convert it to a floating point number. If successful, it is left on the stack (if executing) or compiled (if compiling) as a 32-bit literal. *TRACE.LIT and *TRACE.2LIT check the TRACE flag and compile the appropriate trace instruction if necessary. If the string at POCKET cannot be converted to an integer or floating point number, (ERROR) prints the unrecognized word followed by a question mark and executes ABORT. As discussed above, ABORT does some clean-up and calls QUIT to re-enter the interpreter.

 

Word

INTERPRET calls WORD to fetch the next word in the input stream. WORD ignores leading spaces, parses the next set of characters bounded by a specified delimiter from the input stream, clamps the length of the word to 31, and moves the parsed characters as a counted string to a buffer named POCKET in common ram. The stack picture is

WORD ( delimiting.char – xaddr )

where xaddr is the extended address of POCKET in common memory.

WORD decides where the current input stream is by examining the contents of the user variable BLK. If BLK contains 0, the input stream is the TIB starting at the offset >IN. If BLK is non-zero, the input stream is the mass memory block specified by the contents of BLK, and the offset into the block is >IN .

WORD leaves the offset >IN pointing at the first character beyond the terminating delimiter, unless the end of the input buffer has been reached, in which case >IN points one byte past the last valid character in the input stream. WORD moves the terminating delimiter with the string to POCKET; this terminating delimiter is not included in the count. An unchecked error occurs if the count is greater than 31; WORD simply clamps the count to a maximum of 31 characters. If the input stream is exhausted when WORD executes, WORD stores a count of zero at POCKET.

 

FIND

*(INTERPRET) calls

(#FIND) ( $addr – [ xcfa\xnfa\+/-1 ] or [ 0 ] )

to search the name area of the dictionary for a match of the string at POCKET. The input to this word, $addr (a 16 bit address), is the address of POCKET in common memory. (#FIND) first converts the characters in the string at POCKET to upper case. All headers are stored by QED-Forth using upper case characters. (#FIND) performs the case conversion so that matches will be recognized independent of the case of the characters in the input stream.

(#FIND) searches the CONTEXT and CURRENT vocabularies (in that order) attempting to match the parsed string. It changes pages as necessary to follow the linked list of names and, like all well-behaved words, restores the original page before terminating. If a match is found, it returns on the data stack the extended code field and name field addresses of the word, and a flag which is +1 if the found word is immediate, or -1 if the word is not immediate. A 0 is returned if the word is not found.

A related word is

(FIND) ( $addr – [ xcfa\+/-1 ] or [ 0 ] )

which simply calls (#FIND) and re-arranges the data stack to drop the extended name field address.

Both (#FIND) and (FIND) take as their inputs the address of a counted string in the common memory. Two related words are

#FIND ( <name> – [ xcfa\xnfa\+/-1 ] or [ 0 ] )
FIND ( <name> – [ xcfa\+/-1 ] or [ 0 ] )

These operate by invoking BL WORD to remove the next word (designated as <name> in the stack pictures) from the input stream, and then calling (#FIND) or (FIND), respectively. These words abort if the input stream is exhausted (i.e., if the end of the input line or block is encountered) while searching for the next word in the input stream.

Note that the parentheses in these names indicate that the word performs a subsidiary function, as opposed to indicating a single-page variant of a word as with @ and (@).

 

CREATE

Each time a new word is defined, (CREATE) is called to append the new header to the linked vocabulary list. The two FORTH words

(CREATE) ( $addr – ) \ $addr = pocket {in common memory}
CREATE ( <name> – ) \ removes <name> from input stream

are related. CREATE executes

BL WORD ( <name> – x$addr )

to remove the next space-delimited <name> from the input stream, and then calls (CREATE) to append the header to the name list. Appendix C details the layout of each header in the name list.

(CREATE) converts the parsed string to upper case letters and calls (#FIND) to check whether such a word already exists in the dictionary. If so, a "<name> isn't unique" message is printed (if the UNIQUE.MSG flag in the system user area is ON), and execution continues. (CREATE) calculates the proper link and saves it in the header's link field, stores the current dictionary pointer as the header's code field address, and moves the appropriate number of characters from the name (determined by the user variable WIDTH) into the header. It then updates the contents of the CURRENT vocabulary handle to point to the extended name field address of the newly created word and increments the name pointer NP to point to the next available byte in the vocabulary.

When the created word <name> is executed, the code at its code field address will be executed. Note that CREATE does not write any code in the definitions area; it is the responsibility of other words (usually, the word that calls CREATE, such as : or CODE) to provide for the compilation of executable code in the code field.

CREATE checks whether it successfully stored the header information in the name area; if not, it ABORTs with the "Can't compile– pointer not in RAM" message. This means that the name pointer does not point to modifiable memory. The other compilation primitives C, and , and V, and VC, also have this error checking feature. CREATE also aborts if a page boundary was crossed while the header was being written. The linked list of names can traverse multiple pages, but an individual header cannot cross a page boundary.

 

Creation of Local Variables

CREATE is used in the definition of local variables to create temporary headers appended to the CURRENT vocabulary which are used during compilation of the word containing the locals. All of the characters are saved in the names of the local variables to minimize non-unique names. Once the definition is complete, the names of the defined local variables are no longer needed. The terminating ; of the definition resets the contents of the CURRENT vocabulary handle and NHERE so that the headers of the locals cannot be found and are written over by subsequent headers.

LOCALS are defined into the CURRENT vocabulary for simplicity of implementation. Several consequences of this design decision must be kept in mind. A colon definition containing locals should not call CREATE or other defining words, and should not explicitly manipulate the contents of CURRENT. Messages warning of non-unique names will occur if a local is given a name that conflicts with a previously named non-local variable. Following a local variable naming convention such as using the & prefix for all locals is recommended to avoid this problem.

 

Colon and Semicolon

The word : (colon) starts the definition of a Forth word. It removes the next <name> from the input stream, enters the compilation mode, clears the LOCALS.ARE.COMPILING flag, CREATEs a header for <name>, SMUDGEs the header so it cannot be found, and sets the CSP (check stack pointer) user variable.

The word ; (semicolon) terminates a high level definition. It makes sure that the machine is in compile mode, checks CSP to ensure that the stack depth is unchanged since : executed, and toggles the SMUDGE bit so that the <name> can be found. If locals were compiled, it clears the LOCALS.ARE.COMPILING flag, restores LATEST and NHERE from the system variables LOCALS.LATEST and LOCALS.NHERE, and compiles *;LOCALS to terminate the definition. If locals were not in use, it compiles an RTS to terminate the definition. It then enters the execution mode.

 

Vocabularies

Vocabularies allow the linked list of names in the dictionary to be structured like a tree with branches, where each branch is a separate vocabulary. The dictionary search word (#FIND) starts at a specified point in the linked list at the outer edge of a branch (i.e., at the last word in a specified vocabulary), and searches the list until either the sought-after word is found or the end of the linked list is encountered.

There are two benefits to having this branched structure. First, dictionary searching is faster if fewer words have to be searched. Infrequently accessed words, such as those associated with the in-line assembler, can be placed in a separate vocabulary outside of the main linked list, so the assembler names do not have to be traversed on each search. Second, vocabularies allow the programmer to assign different meanings to identical names. The programmer can control which meaning is found by invoking the appropriate vocabulary.

The kernel has one large vocabulary named FORTH. The ASSEMBLER vocabulary contains the opcodes for the in-line assembler; this vocabulary branches from the base of the FORTH vocabulary. In fact, the only word that is common to both vocabularies is FORTH, which is the bottom word in each of the vocabularies.

The implementation of vocabularies involves several levels of indirection. The dictionary search routine (#FIND) needs to know the extended name field address (xnfa) of the word at the tip of the branch where the search begins. This xnfa is stored in an xhandle that is associated with the vocabulary to be searched. The xaddress of this xhandle is stored in one of two 32-bit user variables named CURRENT and CONTEXT.

CURRENT contains a vocabulary xhandle that specifies to which vocabulary the next defined word is added. CONTEXT contains a vocabulary xhandle that specifies the vocabulary to be searched first. The FIND routines named FIND, #FIND, (FIND), and (#FIND) search the CONTEXT vocabulary first and then, if the contents of CONTEXT and CURRENT are different, search the CURRENT vocabulary.

The contents of the xhandle of the current vocabulary are updated by CREATE. As each new word is created, CREATE updates the xhandle pointed to by CURRENT so that it points to the xnfa of the latest word defined.

Executing CURRENT X@ yields the vocabulary xhandle of the current vocabulary, and executing an additional X@ yields the xnfa (i.e., the extended address of the count byte in the header) of the latest word defined. An equivalent definition of LATEST is

: LATEST ( -- xnfa.of.latest.word.defined )
    CURRENT X@ X@
;

Executing ID. would then print the name of this routine.

 

Creating a New Vocabulary

The defining word

VOCABULARY ( <name> – )

allocates and initializes a 32-bit xhandle in the variable area and creates a child word that, when executed, stores the address of this xhandle into the user variable CONTEXT. Thus executing <name> determines the vocabulary to be searched first. The contents of the xhandle are initialized to the address returned by executing LATEST, which is the extended name field address (xnfa) of the last word defined in the CURRENT vocabulary. But the latest word defined is just <name>. This means that the newly defined vocabulary branches away from the main vocabulary at the header of <name>.

Executing the name of a vocabulary stores its xhandle into CONTEXT, thus causing it to be the first vocabulary searched by the FIND words. To define new words into a vocabulary, the vocabulary name should be stated followed by the word DEFINITIONS which puts the contents of CONTEXT (the search vocabulary) into CURRENT (the vocabulary to which new words are appended).

For example, stating

VOCABULARY MY.VOCAB
MY.VOCAB DEFINITIONS

installs the new vocabulary's handle in both CURRENT and CONTEXT, meaning that MY.VOCAB is the search vocabulary, and new words will be appended to it. Note that, at this point, MY.VOCAB is not empty. For example, if this pair of commands is executed after a cold restart, MY.VOCAB contains all of the words in the FORTH vocabulary.

To maximize dictionary search speed or to create a "clean" vocabulary structure, we may want MY.VOCAB to branch from near the base of the trunk vocabulary and contain very few words in common with the FORTH vocabulary. To make this possible, the header for the word FORTH is at the bottom of the trunk vocabulary in QED-Forth. To define words into a nearly empty vocabulary, execute

VOCABULARY MY.VOCAB
NFA.FOR FORTH ' MY.VOCAB X!

This initializes MY.VOCAB's xhandle to point to the xnfa of the word FORTH at the bottom of the linked list of names. This means that the newly defined vocabulary branches from the bottom word in the trunk vocabulary. Then execute

MY.VOCAB DEFINITIONS

to make MY.VOCAB both the CURRENT and CONTEXT vocabularies. At this point, there is only 1 word that can be found in the main dictionary: FORTH. Execute it:

FORTH

to make FORTH the search vocabulary so that all of the kernel words can be found. Now any previously defined word can be used to define words into the new vocabulary. The new definitions are added to the small CURRENT vocabulary which is MY.VOCAB.

 

Recursion

A recursive definition is one that calls itself. The immediate word RECURSE compiles a call to the currently compiling word into the current definition. Recurse is defined as

: RECURSE    ( -- )
    LATEST NFA>CFA COMPILE.CALL
;
IMMEDIATE

RECURSE makes it easy to define recursive routines. Chapter 9 (Designing Re-entrant Code) discusses the conditions that must be met to ensure that a definition is re-entrant with respect to recursion.

 

Creation of a Synonym

It is easy to define a synonym of an existing word that performs with the same run-time speed as the original word. Simply set the definitions pointer equal to the extended code field address of the word to be imitated and call CREATE to define the synonym. CREATE sets the xcfa of the synonym equal to the contents of DP. For example, the following code defines UNDER as a synonym for -ROT.

HERE                ( -- xaddr | save current DP = xaddr )
CFA.FOR -ROT DP X!    \ set DP to xcfa of -ROT
CREATE UNDER        \ create header whose xcfa is same as -ROT
DP X!                ( -- ) \ restore original DP
 

Using [COMPILE] and COMPILE

[COMPILE] forces the compilation of an immediate word, thereby deferring its run-time behavior. It is used as:

: <name1>
    ...  [COMPILE] <name2>   ...
;

where <name2> is typically immediate, and <name1> is typically not immediate. [COMPILE] removes <name2> from the input stream and causes <name2> to be compiled into the definition of <name1>. Note that if [COMPILE] were not present, <name2> would have been executed instead of being compiled. For example, take another look at the definition of *(INTERPRET) presented earlier in this chapter. [COMPILE] was used to compile the immediate word LITERAL into the definition of *(INTERPRET). As another example, suppose we want to define the word TO.VAR that removes the name of a 16-bit variable from the input stream and stores a value into it:

: TO.VAR    ( n <name>  -- )
    [COMPILE] '   !
;

The contents of a variable are stored at its xpfa which is returned by ' (tick). But ' is immediate, so [COMPILE] is used to force its compilation into the definition of TO.VAR. We could then state

VARIABLE NUMBER.OF.APPLES
1234 TO.VAR NUMBER.OF.APPLES

to store the value 1234 into the variable NUMBER.OF.APPLES.

The word COMPILE is used to defer compilation of a non-immediate word. It is used as:

: <name1>
    ...  COMPILE <name2>   ...
;

where <name1> is typically immediate, and <name2> is not immediate. COMPILE removes <name2> from the input stream and causes <name2> to be compiled when <name1> is later executed. Note that if COMPILE were not present, <name2> would have been executed instead of being compiled when <name1> was later executed. For example, the word IF compiles a headerless run-time routine called 0BRANCH (pronounced zero-branch) into the current definition. It is 0BRANCH that checks the flag on the stack and decides whether to branch. The immediate word IF must compile 0BRANCH and leave some values on the stack to be resolved by ELSE or ENDIF. The equivalent high-level definition of IF is:

: IF     ( -- addr\n )  \ while compiling
    COMPILE 0BRANCH    \ 0BRANCH will be compiled when IF executes
    HERE 0 ,            \ leave addr for ELSE and advance DP
    IF/ELSE.MARKER         \ leave error-checking flag
;
IMMEDIATE

When IF is executed, 0BRANCH does not execute. Rather, COMPILE forces 0BRANCH to be compiled into the current definition. For example, suppose we define a word that contains IF as:

: SAY.HI?     ( flag -- )
    IF
        ." HI" CR
    ENDIF
;

When SAY.HI? is compiled, IF executes, causing 0BRANCH to be compiled into the definition of SAY.HI?. When SAY.HI? is later executed, 0BRANCH executes, tests the flag on top of the stack, and decides whether to print "HI".

 

Forward References and Redefinition

FORTH allows you to define new words in terms of previously defined words. But it is sometimes convenient to define a word that calls a function that will be defined later in the compilation process. Calling a yet-to-be-defined word is known as a "forward reference". The QED-Forth routines NO.OP and REDEFINE facilitate forward references.

For example, assume we're defining an initialization word that sets up an interrupt vector for the software interrupt (SWI), but that it isn't convenient to define the service routine for the SWI until later in the program. We define a "dummy" routine that later will be redefined to execute the real service routine:

: (SWI.SERVICE)
    NO.OP
;

Now the initialization word can reference the service routine. For example,

: IRQ.INITS
    CFA.FOR (SWI.SERVICE)  SWI.ID  ATTACH
    ... < other initializations can go here> ...
;

After the action to be associated with (SWI.SERVICE) is defined:

: SWI.ACTION
    ... < definition goes here> ...
;

the forward reference can be resolved by placing the xcfa of the action word under the xcfa of the dummy word and calling REDEFINE as

CFA.FOR SWI.ACTION CFA.FOR (SWI.SERVICE) REDEFINE

The dummy word (SWI.SERVICE) contains a call to NO.OP. REDEFINE substitutes a call to SWI.ACTION for the call to NO.OP in the definition of (SWI.SERVICE). Thus all words that call (SWI.SERVICE) end up executing the code of SWI.ACTION. Make sure that the code field of (SWI.SERVICE) does not reside in write-protected memory when the REDEFINE instruction is executed; otherwise the definition cannot be changed.

REDEFINE is also useful during debugging. For example, suppose that you have discovered a bug in a single word in the code you have downloaded to the QED Board. After fixing the bug in the faulty definition, you could again download all of your code. It would be faster, however, to simply send over a corrected version of the faulty word and use REDEFINE to install a call to the corrected action in the original word so that all compiled calls to the buggy routine execute the debugged version instead. The redefinition is accomplished as:

CFA.FOR <debugged.definition.name>
CFA.FOR <buggy.definition.name>
REDEFINE

Two requirements must be met: the code field of the buggy definition must be in modifiable RAM, and must be at least 9 bytes long so that the redefinition does not overwrite other words in the dictionary.

 

Expansion of Addressable Memory to 8 Megabytes

The 68HC11 processor has a 16-bit address bus, which means it can address 216 bytes, or 64 Kbytes. The processor's assembler instructions can handle 16-bit addresses. This amount of memory is too limited for many applications, so the QED hardware and software have been designed to increase the memory space to 8 Megabytes. This is accomplished by designating an 8-bit port on the processor (Port G) as a "page latch" that provides 8 additional address lines.

The memory expansion is accomplished using a unique memory map that maximizes run-time efficiency. The page latch allows up to 256 pages of memory to be selected. Each 32 Kbyte page resides in the bottom half (the bottom 32K) of the processor's memory space (256 pages X 32 Kbytes/page = 8 Megabytes of memory). The chip select for the memory device is generated based on the value of the page latch, and the 68HC11 address bits A0 through A14 specify a memory location in a given 32K page.

The top 32K of the 68HC11's memory is called the "common memory" or the "common area"; it is always accessible regardless of the contents of the page latch. The kernel routines that are called most often reside in the common memory, and so are accessible without a page-change operation. The result is that very few page changes are needed when running a program, and this maximizes the execution speed.

The time-critical object code of the kernel resides in the upper 19K of the common area, and the RAM in the remaining 13K is referred to as the "common RAM". The operating system reserves 1K of this RAM and the 68HC11's on-chip EEPROM overlays 1/2K of the RAM, leaving approximately 11.5K available for the user. The 256 byte "user area" that holds QED-Forth's memory pointers and system parameters, as well as data and return stacks and the POCKET buffer must reside in the common RAM.

 

How QED-Forth Changes Pages

Non-kernel definitions usually reside in one of the pages of memory, as opposed to the common memory. Thus the programmer is not free to manipulate the contents of the page latch inside a definition. An example will illustrate this point. Let us say that a programmer wants to fetch a value from a specified address, add 9 to it, and leave the result on the data stack. A valid definition is:

: @9+  ( xaddr -- n | n = {contents of xaddr} + 9 )
    @ 9 +
;

Assume that this word's definition code is on page 4. The kernel word @ takes care of changing and restoring the page from its original value (which is the page on which the object code of @9+ is stored, page 1 in this example) to the page specified in the xaddr, and back again to page 4. Note that it is important that @ restores the page to 4 so that the processor can access the instructions that comprise the remainder of the @9+ definition.

An incorrect way of implementing the same word as follows:

: @9+  ( xaddr -- n | n = {contents of xaddr} + 9 )
(PAGE.LATCH) (C@) >R     ( addr\page -- | save prior page on rstack)
(PAGE.LATCH) C!        ( addr -- | set new page )
(@)                ( -- contents.of.addr )
9 +                 ( -- contents.of.addr+9 )
R> (PAGE.LATCH)   (C!)     \ restore prior page
;

This word works properly if its object code happens to be compiled into the common memory (although it executes more slowly than the original definition). If the object code is in paged memory, however, the program crashes as soon as the page latch is set to a new value (on the second line of the definition above). At this point, the object code of the word is no longer readable by the processor because the page no longer corresponds to the page of the object code.

The moral of this story is that memory accesses from pages other than the currently executing definition's page must be performed by words whose code is in common memory. The common memory acts as a "stepping stone" during page changes. The easiest way to access an address on a different page is to call a kernel routine to perform the memory access.

 

Compilation of Page Changes

Sometimes a word needs to call another word whose object code is on a different page. The QED-Forth interpreter recognizes this situation at compile time and compiles an instruction that properly executes the page change at run time.

The interpreter and the CALL routine invoke the word

COMPILE.CALL ( xcfa – )

to perform the compilation of the specified word into the definitions area. COMPILE.CALL examines the xcfa of the word being compiled. If the xcfa is on the current definitions page or in common memory then the word (C0MPILE.CALL) is invoked; it simply compiles a JSR <cfa> instruction into the definitions area.

If the xcfa is not in the common memory and its page is not equal to the current definitions page (where the call is being compiled), then a more complex calling procedure is compiled. COMPILE.CALL compiles assembler instructions that load accumulator B with the page of the routine to be called, and load accumulator X with the cfa of the routine to be called. COMPILE.CALL then compiles a JSR (jump to subroutine) to a headerless routine called CHANGE.PAGE which expects the cfa and page in X and B, respectively, and executes the call at run time. Because this CHANGE.PAGE routine has no header, the end user cannot directly execute it. QED-Forth compiles references to this routine wherever it is needed.

CHANGE.PAGE first saves the contents of the page latch on the return stack, then sets the page to the value stored in accumulator B, then executes a JSR to the address stored in register X, thereby calling the desired routine. The RTS at the end of that routine returns control to CHANGE.PAGE, which pops the saved page off the return stack and stores it to the page latch, and returns. Because the return stack and the definition code of CHANGE.PAGE are in the common memory, the processor can always access the next instruction as the page change is implemented.

Page changing is fast. The CHANGE.PAGE routine consists of only 7 assembler commands, and executes in 28 cycles (14 usec if the board is clocked at 8 MHz) which is comparable to the execution time of a low-level stack operation such as SWAP.

 

Subroutine Threaded Code

In the interest of conserving memory, most FORTHs are implemented as "indirect threaded" languages. At compile time, lists of addresses are compiled to specify execution order. At run time, a small routine called the "inner interpreter" fetches the address of the next routine to be executed, performs nesting and de-nesting, and calls the appropriate routines. The inner interpreter is a compact loop that requires approximately 25 usec on the 68HC11 for each FORTH word invoked.

QED-Forth uses subroutine threading, an alternative way to compile Forth code. Instead of compiling lists of addresses, QED-Forth compiles actual machine code calls. These typically consist of the opcode for JSR (jump to subroutine) followed by the code field address of the word to be called. The object code is executable machine code, and the processor's return stack keeps track of subroutine nesting. The cost of this scheme is that each call requires 3 bytes (1 byte for JSR plus 2 bytes for the address) compared to 2 bytes for standard threading. Consequently, subroutine threaded object code requires 50% more memory than standard threaded code.

The advantage of subroutine threaded code is speed. With subroutine threaded code there is no need for a FORTH inner interpreter; thus there is no 25 usec overhead associated with the invocation of each word. Owing to the hierarchical nature of FORTH, much of the processor's time is spent executing low-level "primitives" of the kernel that perform stack operations, comparisons, branches, etc. The average execution time of these assembler-coded routines is slightly under 20 usec. Subroutine threaded code executes calls to these routines in approximately 20 usec (including 3 usec for the call via the JSR instruction) while threaded interpretive code requires over 40 usec (including 25 usec for the call via the inner interpreter). Thus QED-Forth's subroutine threading halves the execution time of FORTH compared to standard threading. Moreover, the speed improvement becomes more pronounced as the depth of nesting increases.

 
This page is about: Forth Language Interpreter Compiler and Dictionary – This chapter discusses implementation of QED Forth interpreter and compiler, vocabularies, recursion, deferred compilation, synonym creation, forward references, memory expansion, page changing, and subroutine threaded code. The QED Forth Interpreter …
 
 
Navigation