StrongForth provides the complete Block and Block Extension word sets as specified by ANS Forth. All blocks are contained in a single file with the name forth.blk, which is located in the same directory as the executable forth.exe. However, you can chose to call strongForth with a different block file, if you supply its path and filename as a command-line parameter. To generate a new block file, just copy forth.blk and rename it. Then start strongForth with the new block file as command line parameter.
Block 20 of each block file has a special meaning. This block is automatically LOADed immediately after strongForth has been started. It normally contains source code to display a startup message and to LOAD other blocks that contain the source code of libraries, additional word sets and useful utilities. Executing QUIT at the end of block 20 starts the interpreter loop. If QUIT is replaced with BYE, strongForth simply exits after the interpretation of block 20. This feature is useful for running applications implemeted in strongForth:
C:\>copy forth.blk app.blk 1 file(s) copied. C:\>forth app.blk 8086 strongForth 1.0 20 CLEAR OK \ ... edit block 20 ... OK 20 LIST 0 \ Dummy application 1 2 800 830 THRU \ library 3 4 ." strongForth application start" CR 5 6 \ ... LOAD ... 7 8 ." strongForth application end" BYE 9 10 11 12 13 14 15 OK BYE C:\>forth app.blk strongForth application start strongForth application end C:\>
Physical transfer of blocks to and from the block file is performed by the low-level word R/W:
R/W ( CDATA -> CHARACTER UNSIGNED FLAG -- )
R/W expects a buffer address CDATA -> CHARACTER in the DATA memory area, the number UNSIGNED of the blocks to be transferred, and a FLAG indicating the direction of the transfer. If FLAG is FALSE, R/W reads the block with number UNSIGNED from the block file and stores it starting at address CDATA -> CHARACTER. If FLAG is TRUE, R/W writes the contents of the buffer located at address CDATA -> CHARACTER to the block with number UNSIGNED of the block file. Note that the buffer address is a character address, which indicates that blocks are primarily used for storing characters. However, it is also possible to store any other data in blocks, e. g., arrays of numbers. In these cases, an explicit type cast is required. R/W is supposed to be a low-level word, because ANS Forth specifies some more convenient words that provide read and write access to blocks. You will probably never use it.
#BLOCKS is a constant containing the size of the block file in blocks. This means, block numbers may be between 1 and #BLOCKS. Any attempt to read or write a block outside this range will result in an exception being thrown:
WORDS #BLOCKS #BLOCKS ( -- UNSIGNED ) OK #BLOCKS . 1000 OK 1000 BLOCK DROP OK 0 BLOCK 0 BLOCK ? invalid block number CDATA -> CHARACTER 1001 BLOCK 1001 BLOCK ? invalid block number CDATA -> CHARACTER
Although R/W allows specifying any buffer address, the current version of strongForth uses only one buffer for all transfers. BLK-BUFF is the (constant) address of this buffer:
BLK-BUFF ( -- CDATA -> CHARACTER )
The block buffer can be in either of the following states:
Variable BLK-STAT contains a number of data type SIGNED that indicates the state of the block buffer.
BLK-STAT ( -- DATA -> SIGNED )
As long as the block buffer is unassigned, BLK-STAT contains zero. Any positive value between 1 and #BLOCKS is the number of an unmodified block that has been assigned to the block buffer. A negative value indicates a modified block:
BLK-STAT | assigned to | modified |
---|---|---|
n = 0 | none | n/a |
n > 0 | block n | no |
n < 0 | block |n| | yes |
Once the block that has been assigned to the block buffer is modified, and the modification is intended to be permanant, you need to mark the block as modified by negating the contents of variable BLK-STAT. This can simply be done by executing UPDATE:
: UPDATE ( -- ) BLK-STAT @ ABS NEGATE BLK-STAT ! ;
Note that UPDATE does not write the contents of the block buffer back to the block file. It just marks the block buffer as modified. The actual write operation is performed by SAVE-BUFFERS:
: SAVE-BUFFERS ( -- ) BLK-STAT @ 0< IF BLK-BUFF BLK-STAT @ ABS DUP BLK-STAT ! CAST UNSIGNED TRUE R/W THEN ;
SAVE-BUFFERS first checks whether the block buffer is assigned to a block that has been modified. If this is true, it writes the block back to the block file and marks the block buffer as unmodified.
EMPTY-BUFFERS, if executed before SAVE-BUFFERS, discards all modifications and unassigns the block buffer. FLUSH also unassigns the block buffer, but it previously saves the contents of the block buffer to the block file, if it has been modified. Just like UPDATE and SAVE-BUFFERS, these two words are part of the ANS Forth Block and Block Extension word sets:
: EMPTY-BUFFERS ( -- ) +0 BLK-STAT ! ; : FLUSH ( -- ) SAVE-BUFFERS EMPTY-BUFFERS ;
But how does does the block buffer get assigned to a block? One possibility is to create a new block from scratch with BUFFER:
: BUFFER ( UNSIGNED -- CDATA -> CHARACTER ) DUP BLK-STAT @ ABS CAST UNSIGNED = IF DROP ELSE SAVE-BUFFERS CAST SIGNED BLK-STAT ! THEN BLK-BUFF ;
BUFFER is an ANS Forth word that expects a block number on the data stack and returns the address of a block buffer. It does not read an existing block from teh block file. If the block buffer is already assigned to a different block and has been modified, BUFFER saves the block buffer and thes reassigns it to the new block. Since strongForth has only one block buffer, the output parameter of BUFFER is always the same.
The second possibility to assign the block buffer to a block is to access an already esisting block. the definition of BLOCK is actually very similar to the definition of BUFFER:
: BLOCK ( UNSIGNED -- CDATA -> CHARACTER ) DUP BLK-STAT @ ABS CAST UNSIGNED = IF DROP ELSE SAVE-BUFFERS BLK-BUFF OVER FALSE R/W CAST SIGNED BLK-STAT ! THEN BLK-BUFF ;
The difference is that BLOCK performs a physical read operation after saving the old contents of the block buffer. Of couse, this is only necessary if the new block is not the same as the old one.
According to the ANS Forth specification, a block is just 1024 characters of data on mass storage, designated by a block number. However, it is usually interpreted as being divided into 16 lines of 64 characters each. StrongForth defines two constants for the number of characters per block and per line:
1024 CONSTANT C/B 64 CONSTANT C/L
The contents of a block can be displayed with the ANS Forth word LIST. LIST stores the block number in variable SCR for later reference. An example of the usage of LIST can be seen in the first section of this chapter.
0 VARIABLE SCR : LIST ( UNSIGNED -- ) SCR ! BASE @ DECIMAL SCR @ BLOCK [ C/B C/L / ] LITERAL 0 DO I +2 .R SPACE DUP I C/L * + C/L -TRAILING TYPE CR LOOP DROP BASE ! ;
A block can be made the input source by storing the block number in the system variable BLK. Together with >IN and SOURCE-ID, BLK is actually a part of the input source specification. This means that the semantics of SOURCE needs to be extended. The extended version additionally considers the content of BLK:
0 VARIABLE BLK : SOURCE ( -- CDATA -> CHARACTER UNSIGNED ) BLK @ IF BLK @ BLOCK C/B ELSE SOURCE THEN ;
If BLK contains a block number, SOURCE returns a character string with the address of the block buffer and the number of characters in a block. Otherwise, i. e., if BLK is zero, the input source is determined by the non-block version of SOURCE, which only considers system variable SOURCE-ID in order to decide whether the user input device or a string is the input source. The following table provides a quick overview on all input sources:
BLK | SOURCE-ID | Input Source |
---|---|---|
0 | 0 | user input device |
0 | -1 | string |
n = 1 ... #BLOCKS | don't care | block n |
The introduction of blocks has also consequences for other strongForth words that deal with the input source specification. Since QUIT is supposed to make the user input device the input source, its semantics is extended by initializing BLK:
: QUIT ( -- ) 0 BLK ! QUIT ;
REFILL needs to be modified as well. Refilling the input source with a block means that the succeeding block becomes the new input source. REFILL returns FALSE if the current block has no successor:
: REFILL ( -- FLAG ) BLK @ IF BLK @ #BLOCKS < DUP IF 1 BLK +! 0 >IN ! THEN ELSE REFILL THEN ;
Whenever the current input source specification is saved and restored, the content of BLK has to be included. This rule applies to SAVE-INPUT, RESTORE-INPUT, EVALUATE, (CATCH) and THROW. Consequently, the non-block versions of these words have to be either replaced by corresponding block versions, or their semantics has to be extended:
: SAVE-INPUT ( -- INPUT-SOURCE ) >IN @ BLK @ MERGE CAST INPUT-SOURCE ; : RESTORE-INPUT ( INPUT-SOURCE -- FLAG ) SPLIT CAST UNSIGNED BLK ! CAST UNSIGNED >IN ! FALSE ; : EVALUATE ( CDATA -> CHARACTER UNSIGNED -- ) BLK @ LOCALS| B | 0 BLK ! EVALUATE B BLK ! ; : (CATCH) ( TOKEN INTEGER -- SIGNED ) BLK @ >R >IN @ >R SOURCE-ID >R SP@ -> SINGLE SWAP + >R HANDLER @ >R RP@ HANDLER ! (EXECUTE) R> HANDLER ! R> DROP R> DROP R> DROP R> DROP +0 ; : THROW ( SIGNED -- ) DUP IF HANDLER @ 0= IF ERROR ELSE HANDLER @ RP! RP@ -> DATA @ HANDLER ! RP@ -> SIGNED ! RP@ -> DATA -> SIGNED 1+ @ 1+ SP! RP@ -> SIGNED @ ( SIGNED -- )CAST RP@ 2 CELLS + -> SIGNED @ TO SOURCE-ID RP@ 3 CELLS + -> UNSIGNED @ >IN ! RP@ 4 CELLS + -> UNSIGNED @ BLK ! (RDROP) (RDROP) (RDROP) (RDROP) (RDROP) THEN ELSE DROP THEN ;
The non-block version of SAVE-INPUT just returns the content of >IN in an item of data type INPUT-SOURCE. The block version additionally saves the content of BLK. The single-cell values in >IN and BLK are merged into one double-cell item of data type INPUT-SOURCE. RESTORE-INPUT reverses this operation. It splits the item of data type INPUT-SOURCE into the original single-cell values and then restores the contents of variables >IN and BLK.
EVALUATE temporarily makes a string the input source. If the current input source is a block, EVALUATE has to store zero in BLK. At the end, the previous value of BLK is being restored.
The exception frame created by (CATCH) has to be extended by one cell containing the value of BLK. (CATCH) stores the value of BLK in the exception frame, while THROW uses this value to restore BLK as part of the input source specification. This is the memory image of the extended exception frame:
value of BLK |
value of >IN |
value of SOURCE-ID |
data stack pointer after CATCH |
pointer to previous exception frame |
If the input source is the user input device or a string, \ discards the text up to and including the next occurence of the character \, or the remainder of the parse area if it doesn't contain another backslash. If the input source is a block, the semantics of \ is slightly different. Only the remainder of the current line of text is considered. Here's the definition of the extended version of \:
: \ ( -- ) BLK @ IF >IN @ C/L / 1+ C/L * POSTPONE \ >IN @ MIN >IN ! ELSE POSTPONE \ THEN ; IMMEDIATE
The final word that needs to be modified in order to support blocks as input source is ERROR (see chapter 16):
: ERROR ( SIGNED -- ) CASE -1 OF ENDOF -2 OF ERROR-ADDR @ ERROR-COUNT @ TYPE ENDOF CR SOURCE DROP >IN @ DECIMAL BLK @ IF DUP DUP C/L MOD - /STRING THEN -TRAILING TYPE DUP -303 -0 WITHIN IF ." ? " DUP ABS CAST UNSIGNED C/L C/B */MOD 1+ BLOCK SWAP + C/L -TRAILING TYPE SPACE ELSE ." ? ERROR " DUP . THEN BLK @ IF BLK @ . >IN @ C/L / . THEN CR POSTPONE .S ENDCASE ABORT ;Out of the three parts of the error message,
the first two are affected. The phrase BLK @ IF DUP DUP C/L MOD - /STRING THEN ensures that only one line of the block is printed to indicate the error's location within the parse area. The error message in a narrow sense is much more elaborate. Instead of just printing "ERROR" and the error number, the block version of ERROR prints a descriptive message that is obtained from blocks 1 to 19. Each line of these blocks contains one error message. E. g., block 1 contains the error messages for error codes -3 to -15:
1 LIST 0 \ strongForth error messages 1 2 3 stack overflow 4 stack underflow 5 return stack overflow 6 return stack underflow 7 do-loops nested too deeply during execution 8 dictionary overflow 9 invalid memory address 10 division by zero 11 result out of range 12 argument type mismatch 13 undefined word 14 interpreting a compile-only word 15 invalid FORGET OK
Remember that error codes -1 and -2 have a special meaning. Since block 19 is the last block containing error messages, -303 is the lowest error number for which an error message is available. For all error numbers outside the range from -303 to -1, the same error message as in the non-block version of ERROR is printed:
-1000 ERROR ? ERROR -1000
The phrase BLK @ IF BLK @ . >IN @ C/L / . THEN prints the block number and the line number within the block.
The ANS Forth word LOAD can be used to interpret the contents of a block. LOAD saves the input source specification as locals on the return stack, stores the block number in variable BLK, resets >IN and interprets. When done, it simply restores the previous input source specification. Here's the definition of strongForth's version of LOAD:
: LOAD ( UNSIGNED -- ) DUP 1 #BLOCKS 1+ WITHIN IF >IN @ BLK @ LOCALS| B I | BLK ! 0 >IN ! INTERPRET B BLK ! I >IN ! ELSE DROP -35 THROW THEN ;
You may notice that LOAD checks the input parameter for being a valid block number. Since R/W already checks for valid block numbers, is this additional check really necessary? It is. If INTERPRET is executed with an invalid block number, R/W would throw an exception. If no exception frame exists, ERROR handles the exception. ERROR tries displaying the source line that caused the exception, which it assumes to be in the block whose number is stored in BLK. The attempt to access this block will throw another exception that again ends up in ERROR and so on. Sooner or later, one of the stacks overflows and causes the system to crash. This can only be avoided by ensuring that BLK never contains an invalid block number.
THRU allows interpreting a sequence of blocks. It's definition contains nothing more than a check for valid input parameters and a loop around LOAD:
: THRU ( UNSIGNED 1ST -- ) OVER OVER > IF DROP DROP -35 THROW ELSE 1+ SWAP DO I LOAD LOOP THEN ;
Dr. Stephan Becher - December 8th, 2005