CHAPTER III. THE cmFORTH OPERATING SYSTEM
This version of Forth, cmFORTH, was developed by Chuck Moore, the chief architect of the Novix-4000 Forth engine and the inventer of the Forth language. It is used in Chuck's Gamma Board and in Software Composers' SC1000C single board computer as their operating system. Chuck Moore and Novix kindly donated this remarkable software package into the public domain to encourage users exploring the phenominal capabilities of the NC4000 chip. This chapter is intended to be the documentation for cmFORTH. I will attempt to go through the system in minute detail, in order to help those who are unfamiliar with the Forth language as well as those who are not familiar with Chuck Moore's coding style. In any case, the source listing itself is the primary documentation and the descriptions in this chapter are at best commentary to the source listing. The complete source listing of cmFORTH is included in this book as Appendix A. A program to burn target compiled object code into EPROM's is in Appendix B. A short program which must be implemented in a host computer to act as a terminal/disk server for the NC4000 computer is in Appendix C. The original source listing of cmFORTH is your best source of code examples and programming style, when striving to use the NC4000 chip most effectively. You are encouraged to study this listing very carefully in order to gain maximum benefit from the chip. The source code is only 30 screens long. It would not be very difficult to study it and to be thoroughly familiar with it. There are many important innovations in cmFORTH worthy of your special attention. First is the optimizing compiler which takes normal Forth source code and compiles very short and efficient NC4000 machine instructions. Next is the dual threaded vocabulary structure in which all the compiler directives and smart compiling/assembling words are linked together, separate from the regular FORTH vocabulary. The interpreter searches only the FORTH vocabulary. The compiler first searches the COMPILER vocabulary and then the FORTH vocabulary. This searching method eliminates the necessity of immediate words. Lastly, a very simple linking method is used to compile words into a target dictionary, which can later be isolated from the main dictionary and burned into EPROM. These innovations in time will make significant impact in the Forth community, leading to better and more efficient Forth systems and programs. .new
1. The Kernel The kernel in a Forth system is a collection of low level instructions which drive the computer and are used to construct other high level instructions. In cmFORTH, the kernel contains primarily simple NC4000 machine instructions. However, many of the commonly used Forth words do not have corresponding single word NC4000 instructions and they will have to be synthesized from the primitive NC4000 instructions.
1.1. The primitive Forth Words Primitive stack operators are the machine instructions of the NC4000 chip. However, each machine instruction must have two versions, one to be executed by the text interpreter and the other for the compiler. The executable version must be a colon defintion with names so that they can be found by the text interpreter. They can be compiled into other colon definitions, but the resulting code will be terribly inefficient because of the overhead in nesting and unnesting. The other version used by the compiler is smart. It generates optimized machine code whenever feasible, taking advantage of the unique property of NC4000 in combining many Forth words into a single machine instructions. The following primitive Forth words are redefined so that they can be executed by the text interpreter.
: SWAP SWAP ; : OVER OVER ; : DUP DUP ; : DROP DROP ; : XOR XOR ; : AND AND ; : OR OR ; : + + ; : - - ; : 0< 0< ; : NEGATE NEGATE ; : @ @ ; : ! ! ;
Many other commonly used Forth words cannot be reduced to single NC4000 instructions, so they have to be constructed with several NC4000 machine instructions.
: ROT ( n1 n2 n3 -- n2 n3 n1 ) >R SWAP Exchange n1 and n2. R> SWAP Exchange n1 and n3. ;
: 0= ( n -- f ) IF 0 EXIT THEN Return false if not 0. EXIT is cheaper and faster. -1 Can be obtained from a register. ;
: NOT ( n -- f ) 0= Logic NOT, not one's complement. ;
: < ( n1 n2 -- f ) - 0< ;
: > ( n1 n2 -- f ) SWAP- A NC4000 primitive instruction. ;
: = ( n1 n2 -- f ) XOR 0= ;
: U< ( u1 u2 -- f ) - 2/ Get the carry of subtraction. 0< Return proper flag. ;
: ?DUP ( n -- n n | 0 ) DUP IF DUP EXIT THEN EXIT is faster. ;
: WITHIN ( n low high -- f ) OVER - >R high - low - n - low R> U< In range? ;
: ABS ( n -- u ) DUP 0< Negative? IF NEGATE EXIT THEN Invert negative number. ;
: MAX ( n1 n2 -- n1 | n2 ) OVER OVER - n1 - n2 0< Compare. IF SWAP-DROP EXIT n1 < n2, drop n1. THEN DROP Otherwise, drop n2. ;
: MIN ( n1 n2 -- n1 | n2 ) OVER OVER - n1 - n2 0< IF DROP EXIT n1 > n2, drop n2. THEN SWAP-DROP Otherwise, drop n1. ;
The funny IF-BEGIN... UNTIL-THEN structure in Screen 11, connecting the two definitions MAX and MIN, in effect achieves the above function with a net saving of four instructions. You can do it because cmFORTH does not have compiler security and protection. Not recommended for general programming practice.
: 2DUP ( d -- d d ) OVER OVER ;
: 2DROP ( d -- ) DROP DROP ;
1.2. Memory Accessing Words
: +! ( n a -- ) 0 @+ Fetch from a, while keeping a on the stack. >R Save a. + Add n to contents of a. R> ! Store the sum back into a. ;
: 2C@+ ( a -- a+1 l h ) 1 @+ Fetch a cell and increment a. SWAP DUP Get the contents just fetched. 127 AND Isolated the low byte. SWAP 6 TIMES 2/ Right justify the high byte. ;
: 2/MOD ( n -- rem quot ) Equivalent to 2 /MOD but faster. Needed to convert byte address to cell address. DUP 1 AND Get the remainder. SWAP 2/ Shift left to get quotient. ;
: C! ( b a -- ) a is byte address, which has to be converted to cell address. 2/MOD DUP >R Save cell address. @ Cell contents. SWAP Byte offset. IF -256 AND Offset=1. Mask off lower byte. ELSE 255 AND Offset=0. Mask off higher byte. SWAP Get the byte b. 6 TIMES 2* Shift left by 8 bits. THEN XOR Combine two bytes. R> ! Put the cell back. ;
: C@ ( a -- b ) 2/MOD Get the cell address. @ Contents of the cell. SWAP 1 - Offset=1 ? IF 6 TIMES 2/ THEN Yes. Shift right by 8 bits. 255 AND Mask off the high byte. ;
: 2@ ( a -- d ) 1 @+ Get the most significant cell. @ Get the least significant cell. SWAP Put them in correct order. ;
: 2! ( d a -- ) 1 !+ Store the most significant cell. ! Store the next cell. ;
NC4000 is a 16 bit machine and it can access memory only by 16 bit cells. Two bytes are packed into one cell, with the first byte in the higher half (MSB) of the cell. The byte address is twice that of the cell address, with the least significant bit as the byte offset in a cell. To access one byte in the memory, one has to convert the byte address to a cell address by 2/MOD and use the quotient as an offset to find the requested byte. It takes lots of extra work to do byte addressing. Avoid it at all cost. If you really have to get bytes from the memory, the right word to use is 2C@+ which fetches one cell from the memory and returns both bytes on the stack. The address is incremented by 1 and preserved as the third element on the stack so you can fetch the next two bytes. It is faster than C@ and much more powerful.
: MOVE ( a1 a2 n -- ) a1 is the starting address of the source, and a2 is the end address of destination string. Be careful! >R Save cell count. MD I! Save a2 in MD register. I TIMES 1 @+ Fetch n cells to the data stack. MD I@! Retrieve a2. R> Retrieve cell count. TIMES 1 !- Pop cells to destination in reverse order. DROP Discard last address. ;
: FILL ( a # n -- ) Fill a memory range with cell value n. SWAP 1 - >R Push n-1 on return stack as count. SWAP ( n a -- ) BEGIN OVER SWAP ( n n a -- ) 1 !+ Non-destructive store with a incremented. NEXT 2DROP Clear stack. ;
: ERASE ( a # -- ) Zero a range of cells. 0 FILL ;
1.3. Multiply and Divide NC4000 does not have single instruction multiply or divide, which requires a lot of gates to implement. What is provided are multiply steps, divide steps, and square-root steps, which can be used repetitively to achieve the desired results. Problems in processing the carry bit in the prototype chip cause some restrictions in multiplication. The software fixes to perform correctly the proper function are not implemented. You have to work around these bugs. An example is included in Chapter IV. OCTAL
: U*+ ( u1 r u2 -- d ) Unsigned integers u1 and u2 are multiplied and added to r. The product is an unsigned double integer on the stack. Warning: u2 must be even! MD I! Store u2 in MD register. 16 TIMES *' Repeat multiply step instruction *' 16 times and the product is left on the stack. ;
: M/MOD ( ud u -- q r ) Unsigned double integer ud is divided by unsigned integer u. Both quotient and remainder are left on the stack. Note the order of q and r is not standard. MD I! Store u in MD register. D2* Left shift d by 1 bit so that it is always even. 15 TIMES /' Repeat divide step /' 15 times. /'' Last divide step. ;
: -M/MOD ( d u -- q r ) Double integer d is divided by unsigned integer u. OVER 0< Is d negative? IF DUP >R Save u. + Add u to the higher half of d THEN for floored division. M/MOD Do the divide now. ;
: M/ ( d u -- q ) Mixed mode divide. -M/MOD DROP Discard remainder. ;
: M*+ ( u1 0 u2 -- d ) Unsigned multiply. MD I! Copy u2 to MD register. 13 TIMES *' Repeat multiply step. *- Last signed multiply step. ;
: VNEGATE ( n1 n2 -- n3 n4 ) Negate top two integers on the stack. NEGATE Negate top integer. SWAP NEGATE SWAP Negate next integer. ;
: M* ( n1 n2 -- d ) Mixed mode multiplication of two signed integers. DUP 0< Is n2 negative? IF VNEGATE THEN If so, negate both integers. 0 SWAP initialize accumulator. M*+ Do the multiplication. ;
: /MOD ( u1 u2 -- r q ) Divide unsigned integers and return both remainder and quotient. 0 SWAP Insert 0, making dividend a double integer. M/MOD Do the mixed mode divide. SWAP Correct the order of results. ;
: MOD ( u1 u2 -- r ) Find remainder of unsigned integer division. /MOD Do the generalized divide. DROP Discard quotient. ;
: */MOD ( u1 u2 u3 -- r q ) Multiply u1 and u2. Divide the double integer product by u3. Return both remainder and quotient. >R Save divisor u3. 0 SWAP U*+ Multiply u1 and u2. R> M/MOD Divide by u3. SWAP Correct the order of r and q. ;
: */ ( n1 n2 u -- r ) Ratio of n1*n2/u. >R Save u. M* Signed multiply of n1 and n2. R> M/ Divide by u. ;
: * ( n1 n2 -- r ) Signed multiply. 0 SWAP U*+ Signed multiply. DROP Discard high order cell. ;
: / ( n u -- q ) Divide by unsigned integer. >R Save divisor u. DUP 0< Extend the sign of n. R> M/ Divide. ; .new
2. System Variables System variables contain vital information needed by the Forth system to function. Most of them are pointers to various areas in the Forth system, such as the top of the dictionary, the disk buffers, the terminal input buffer, the vocabulary threads, etc. System variables in this implementation are kept at the bottom of RAM space, starting from location 16. Thus the first 16 system variables are in the so called local memory, which can be accessed by single cell instructions. These are the most frequently used system variables. Less frequently used variables are kept above location 35. Following is the list of system variables defined in this implementation, their memory locations, their initial values if initialized, and their function.
PREV Memory 16, not initialized. Pointer to the most recently referenced disk buffer.
OLDEST Memory 17, not initialized. Pointer to the oldest loaded disk buffer.
BUFFERS Memory 18 and 19, not initialized. Storing block numbers of blocks in the disk buffer.
NB A constant of value 1. Number of disk buffers less 1.
BASE Memory 20, initialized to 10. Number base for numeric I/O conversion.
CNT Memory 21, not initialized. Count of characters received from the terminal device.
>IN Memory 22, not initialized. Pointer to the input stream of characters. Used by WORD to parse strings.
BLK Memory 23, initialized to 0. Contains the block number under interpretation.
?CODE Memory 24, initialized to 0. Storing the address of the machine code most recently compiled. Used by the optimizing compiler to construction multi-function instructions.
dA Memory 25, initialized to 0. Memory address offset to be subtracted from the current address so that the dictionary compiled can be relocated to another part of memory.
MSG Memory 26, initialized to the end of the system variable area. Pointer to the terminal input buffer.
CURSOR Memory 27, initialized to 0. Pointer to the memory location where the last input character is stored.
WIDTH Memory 28, initialized to 2. Cells in the name field of an entry in the dictionary.
OFFSET Memory 29, initialized to 0. Block number which became the logic 0 block during disk access.
H Memory 30, initialized to 64 cells after terminal input buffer MSG. Pointer to the top of the current dictionary.
C/B Memory 31, initialized to 417. Machine cycles equivalent to the width of a bit riding the RS-232 terminal port.
Interrupt Vector Memory 32 to 33, not initialized. Machine instructions are stored here to handle interrupt requests.
Thread Table Memory 34 and 35, pointers to the ends of 2 threads in the dictionary. The dictionary and vocabularies are hashed into 2 threads. The name field addresses at the end of each thread are stored in this table for dictionary searching.
CONTEXT Memory 36, initialized to 1. Hash code of the context vocabulary.
¡@ |