Advanced Forth Programming Topics
Program development techniques that make it easy to code instrument control applications
This chapter presents an explanation of many features that make it easy to program in QED-Forth. These include the interactive debugger, floating point mathematics, heap memory manager, arrays and matrices, structures, and multitasking executive.
Program development techniques
Using ANEW
FORTH is a language that encourages interactive and incremental programming and debugging. You fully test low level words, and build up to higher level words. Often you will discover a bug in a word that has been defined, and will want to re-compile a new version of the word to fix the error. The interpreter always finds the most recently compiled version of a word, so you need not worry about entering multiple definitions of a word. But after many re-definitions memory space is wasted as the dictionary becomes cluttered with unneeded buggy definitions. The QED-Forth word ANEW is solves this problem.
The action of ANEW is based on the standard FORTH word FORGET which is explained here. If a definition for a word called GARBAGE is compiled and then you decide to re-compile it, you can execute the command
FORGET GARBAGE ↓
before re-compiling. This changes some user variables so that the interpreter can no longer find any of the words that have been compiled since GARBAGE was defined. Consequently, GARBAGE and all subsequent words are “forgotten”. FORGET resets the definitions pointer DP to point to the first byte in the code field of GARBAGE. Thus the next definition you enter will over-write the original definition. FORGET also sets the name pointer NP to point to the first byte of the header of GARBAGE in the dictionary. This will cause the header for the next word entered to write over the original header for GARBAGE.
But there is a problem with using FORGET: it does not reset the variable pointer VP or the EEPROM variable pointer EEP to the values they held before GARBAGE was defined. So if GARBAGE or any of the words compiled after it advanced VP or EEP, this variable or EEPROM variable space will not be recovered, thus wasting memory.
The word ANEW is a more sophisticated and easy to use word for dictionary cleanup. Here’s an example showing the use of ANEW:
ANEW DEBUG.SESSION
: GARBAGE ( -- )
.” I dont do much”
;
VARIABLE MYVAR
There is a spelling error in the word “don’t” in GARBAGE, so the error is corrected and the source code is re-loaded as:
ANEW DEBUG.SESSION
: GARBAGE ( -- )
.” I don’t do much”
;
VARIABLE MYVAR
There is now only one definition each of GARBAGE and MYVAR in the dictionary; ANEW took care of forgetting the previous definitions and properly resetting NP, DP, VP and EEP. To use it, just put ANEW followed by a “marker” name of your choice (usually one that describes the general function of the code you’re compiling) at the top of the source code file that you download. Then each time you download a new version of the file, ANEW will search the dictionary for the marker name (in this case, DEBUG.SESSION), and intelligently forget back to that point. It will leave the marker name in the dictionary so that future ANEW commands can find it.
Using ANEW <marker.name> as the first command in each source code file provides easy and automatic cleanup of the dictionary during debugging. If the memory map is altered at the start of a source code file, the ANEW command should follow rather than precede the code that changes the memory map. Setting the memory map is described later in this chapter.
How ANEW Works
You need not understand how ANEW works to use it. This description is for those who are interested in operational details.
ANEW searches the dictionary for the specified marker word. If the word is not found, ANEW defines the marker name as a word whose action is to store the current value of VHERE into VP and store the current value of EEHERE into EEP in the user area. Subsequent ANEW commands search the vocabulary for the specified marker name and, finding the marker, execute it to reset VP and EEP, and FORGET the words back to the marker which properly resets NP and DP. ANEW then re-defines the marker word so that future ANEW commands can find it.
Defining an APPLICATION segment
If your program relies on pre-compiled device driver libraries provided by Mosaic, you’ll need to declare these using the REQUIRES syntax described in the Segment Management Chapter. The APPLICATION (or LIBRARY) command works just like ANEW: it expects a unique name (in this case, the name you choose for your application program).
When the source code file containing the APPLICATION command is reloaded, the memory map is automatically set back to the state it was in when the APPLICATION command was initially executed. Unlike the ANEW command, the APPLICATION command typically requires an END.SEGMENT statement to be placed at the end of the program.
For example, let’s say you’re designing a product that includes the AnalogIO Wildcard, and you want to use the pre-coded library called AnalogIO supplied by Mosaic to interface to the Wildcard. To declare an application program named ANALOG_CONTROLLER that requires the loading of the AnalogIO driver, you could execute
APPLICATION ANALOG_CONTROLLER
REQUIRES.FIXED AnalogIO
<your program code goes here>
END.SEGMENT
Your program code can call any of the functions defined in the AnalogIO library. When you reload the program, the APPLICATION statement will perform the ANEW action to simplify your development. For more information about LIBRARY and APPLICATION segments, consult the Segment Management Chapter in this document.
Using ON.FORGET for heap recovery
The one area of memory that ANEW, APPLICATION and LIBRARY do not recover is heap memory associated with forgotten words or data structures. You can accomplish this by defining a word having the (non-unique) name ON.FORGET. FORGET (which is called by ANEW, APPLICATION and LIBRARY) both check the names of all words being forgotten; if the name is ON.FORGET, the definition is executed before being forgotten. For example, the following code creates and dimensions an array and provides for recovery of the heap space when the code is forgotten:
ANEW EXAMPLE
ARRAY: MY.ARRAY
7 1 2 ‘ MY.ARRAY DIMENSIONED \ 1 dimension, 7 2-byte elements
: ON.FORGET ( -- )
‘ MY.ARRAY DELETED
;
When this code is compiled, QED-Forth will report that ON.FORGET is not unique; this is expected and is not a problem. On subsequent execution of ANEW EXAMPLE or even FORGET EXAMPLE, MY.ARRAY will be forgotten, and the ON.FORGET word will be executed before it is forgotten. This will delete MY.ARRAY to recover any heap space that had been allocated to the array.
Summary of the Forth interactive debugger
QED-Forth’s debugger can be used to debug high level and assembly coded routines. The debugger allows you to:
Trace program flow
Single-step through a program or routine
Install break points by simply compiling a call to the BREAK routine
Enter a “break” mode while debugging that allows you to inspect or modify memory locations
Install a customized diagnostic routine that is executed after each program instruction, and
Print the contents of the machine registers after each high level and/or assembly instruction.
The debugger is configured by the logical values of four user variables: TRACE, DEBUG, SINGLE.STEP, and DUMP.REGISTERS. Each of these can be turned ON or OFF by the programmer. If TRACE is ON, QED-Forth compiles a trace instruction before every compiled command. When the traced words execute, the trace instruction checks the DEBUG flag and, if it is ON, prints the stack picture at that point in the program and the name of each traced command. If SINGLE.STEP is ON, the trace instruction stops after each command and enters the BREAK mode which is a FORTH-style interpreter that lets the programmer examine and alter variables, stack items, etc. If DUMP.REGISTERS is ON, the trace instruction prints the contents of the registers so you can see how each instruction affects the machine registers. This is especially useful when debugging assembly-coded routines.
Those who need even more sophisticated debugging tools can define a customized diagnostic action and install it (using the word IS.TRACE.ACTION) as the first action that is executed by each compiled trace instruction.
A Sample Debugging Session
Let’s look at an example of a buggy definition and use the debugger to find and correct the fault. Suppose a word is defined to calculate the sum 1+2+3 as
ANEW DEBUG.SESSION ok
: 1+2+3 ( -- n )
0 LOCALS{ &sum }
3 1 DO &sum I + TO &sum LOOP
&sum ; ↓ ok
1+2+3 . ↓ 3 ok
Execution of the definition shows that it calculates 3 instead of the 6 that is expected. Let’s say that we don’t know what’s wrong, and we aren’t fully sure how the DO LOOP is working, so we decide to TRACE the definition. This is accomplished by setting the user variable TRACE to true, and re-compiling the definition:
TRACE ON ↓
ANEW DEBUG.SESSION ↓
: 1+2+3 ( -- n )
0 LOCALS{ &sum }↓
3 1 DO &sum I + TO &sum LOOP
&sum ; ↓ ok
Because TRACE is ON, the compiler inserts a trace instruction before every command in 1+2+3. At execution time, each compiled trace instruction checks to see if DEBUG is ON. If so, it prints the current stack picture and the name of the currently executing command. Thus we get a step-by-step picture of the execution of the word 1+2+3. In the printout below, the first column is the name of each command which is printed as it is executed. The second column is a stack print in the standard format, with the number of items followed by a list of the items (a maximum of 5 are displayed) separated by the \ character. The third column is commentary added to this document to explain what is occurring.
1+2+3 [ 0 ] execute the word
0 [ 1 ] \ 0 0 is placed on stack
LOCA___ [ 0 ] LOCALS{ executes
3 [ 1 ] \ 3
1 [ 2 ] \ 3 \ 1 limit and index for DO are on stack
DO [ 0 ] start of the loop
&0 [ 1 ] \ 0 local #0 = &sum leaves its value
I [ 2 ] \ 0 \ 1 loop index = 1 is on stack
+ [ 1 ] \ 1 add
TO [ 0 ] update &sum
LOOP [ 0 ] start loop again
&0 [ 1 ] \ 1 &sum
I [ 2 ] \ 1 \ 2 loop index = 2
+ [ 1 ] \ 3 add
TO [ 0 ] update &sum
LOOP [ 0 ] the loop has finished too early!!
&0 [ 1 ] \ 3 leave the answer on the stack
; ok [ 1 ] \ 3 the word is finished
In examining the printout, we see that the loop only executes twice, for I = 1 and I = 2. This trace suggests that instead of 3 1 DO … LOOP in the definition of 1+2+3, we should have used 4 1 DO … LOOP. A corrected version of the routine could now be recompiled.
Note that traces of 16-bit local variables print & (which by convention is usually the first character in a 16-bit local variable’s name) followed by a numeric identifier for the local. Each 32-bit locals is traced as D& (indicating a double-size quantity) followed by the local’s numeric identifier. An identifier is used because the names of the locals are not available at the time the trace is performed; they are forgotten as soon as the definition is compiled.
Also note that no trace instruction is compiled for words that are removed from the input stream by another command. For example, the command TO removes the next word from the input stream, so the local that appears after TO in the definition does not appear in the trace.
TRACE can handle nested definitions. If another word that calls 1+2+3 is defined and if DEBUG is ON, a trace of all the commands in 1+2+3 is printed when 1+2+3 executes in the higher level word. If TRACE is ON during the compilation of both 1+2+3 and the higher level word, the output is indented to indicate the nesting of the words. Try it to see how it works.
The BREAK Mode and Single Step Execution
Assume that a trace of a routine is being printed, and you want to stop and examine the state of some variables that are being modified by the program. You can type any key to enter the
BREAK mode, examine the variables using interactive
Forth commands, and resume the trace where it left off.
The
BREAK mode is a version of the QED-Forth interpreter that displays a BREAK> prompt at the start of every line. When in this mode, you can execute any FORTH command, examine or modify any memory location, or even compile new words. In short, you can do anything that can be done in the standard QED-Forth interpreter. Executing a single carriage return alone on a line exits the
BREAK mode and continues the trace in progress.
BREAK is entered if:
The kernel word BREAK is compiled into a definition.
Any key is typed from the terminal while a word is being traced, or
The user variable SINGLE.STEP is ON, which causes the BREAK mode to be entered after each traced instruction.
The complete register state of the Freescale HCS12 (9S12) on the PDQ Single Board Computer (SBC) is automatically stacked before BREAK is entered, so a CALL to the BREAK instruction can even be compiled into assembly coded routines. When BREAK is exited, the registers are restored to the state that they had before BREAK was called.
To exit the BREAK mode while allowing the trace to continue, type a carriage return alone on a line. Executing ABORT (or causing any error that in turn executes ABORT) exits the BREAK mode and returns control to the standard QED-Forth interpreter, and also halts any trace that is in progress.
We can again execute 1+2+3 and, while the trace is printing, type any key. If the terminal immediately sends the character (some terminals only send the character after delaying a while!), then the BREAK mode will be entered. The contents of the stack or of memory locations can be examined. Each time a command line followed by a carriage return is entered, the commands are executed and the BREAK> prompt is printed at the start of the next line. Typing a carriage return alone on the line resumes the trace.
The variables that control the debugger can be altered from the BREAK interpreter while a trace is in progress. For example, while in the BREAK mode SINGLE.STEP could be turned ON to enter the BREAK mode after each succeeding line of the trace. Or DEBUG could be turned OFF to rapidly complete the execution of the word without printing the remainder of the trace.
Assume that we don’t want a full trace of the word 1+2+3, but we are curious about the value that the command I is leaving on the stack on each pass through the loop. This can be accomplished by editing the word BREAK into the definition just after the I command and recompiling with TRACE OFF. When 1+2+3 is executed the BREAK mode will be entered after each execution of I and the contents of the stack can be examined. A single carriage return continues execution until the next BREAK or until the end of the routine is encountered, and typing ABORT terminates the execution of 1+2+3 and returns to the interpreter.
Printing the Register Contents
If the user variable
DUMP.REGISTERS is
ON, each trace instruction prints a 1-line summary of the register contents. All register contents are displayed in hexadecimal base, regardless of the value in the user variable
BASE. For example, to trace a simple assembly language routine that adds the value 3 to the top item on the data stack, execute
TRACE ON \ tell QED-Forth to compile TRACE instructions
CODE 3+ ( n -- n+3 )
00 IND,Y LDD \ put n into D; Y is the data stack pointer
0003 IMM ADDD \ add 3 to n; result still in D
00 IND,Y STD \ put n+3 on data stack
RTS \ return
END.CODE
This word can now be executed with DUMP.REGISTERS ON to see the effect that each assembly instruction has on the contents of the registers:
DUMP.REGISTERS ON↓ ok
5 3+ ↓ ( 1 ) \ 5
PC= 8006 SP= 18F5 CC= D1 D= 3C1D IX= 8000 IY= 15FE
LDD ( 1 ) \ 5
PC= 800E SP= 18F5 CC= D1 D= 5 IX= 8000 IY= 15FE
ADDD ( 1 ) \ 5
PC= 8017 SP= 18F5 CC= D0 D= 8 IX= 8000 IY= 15FE
STD ( 1 ) \ 8
PC= 801F SP= 18F5 CC= D0 D= 8 IX= 8000 IY= 15FE
RTS ok ( 1 ) \ 8
The address in the program counter (PC) advances with each instruction. The return stack pointer (SP), index register X (IX), and index register Y (IY, used as the data stack pointer) are unchanged by this simple routine. The contents of the condition code register (CC) and of the double accumulator (D) change as the top stack item (5) is loaded into D, and then 3 is added to this quantity. Note that although the data stack pointer IY is unchanged, the contents of the top stack position which is pointed to by IY are modified.
In short, the debugging environment gives you the ability to trace the execution steps in a word, allowing examination of the contents of the stack, registers, or memory locations at any point during the execution of high level or assembly coded words.
Specifying a Customized Trace Action
The capabilities of the debugging system can be further expanded. The programmer may install a customized diagnostic routine as the first action that is performed by each compiled trace instruction. The diagnostic routine must have no net effect on the data or return stacks, and must be compiled while
TRACE is
OFF; if trace is
ON while the trace action is being defined, an infinite loop of calls to the trace routine is initiated. Installation is accomplished by placing the extended code field address of the diagnostic routine on the stack and executing
IS.TRACE.ACTION. The default trace action (set upon a
COLD restart) is
NO.OP which is a do-nothing routine. The
NO.OP routine can be installed as the trace action at any time by executing
DEFAULT.TRACE.ACTION.
The customized trace action can be used to reduce the time spent localizing a hard-to-find bug. If we know that a particular condition is closely associated with the onset of the error during a program’s execution, the trace action can be used to turn
DEBUG ON when the condition is detected, triggering the trace printout.
For example, assume that a complex program contains a mysterious bug, and we are trying to locate it. Further assume that we know that the bug causes a particular variable called
THE.VARIABLE
to be set equal to 0. One way to locate the problem would be to compile the program with
TRACE ON and then execute the entire program with
DEBUG ON. The disadvantage of this is that a great deal of traced output must be examined. A more efficient method is to write a word that tests the variable in question and, if it equals 0 (which signals that the bug has just occurred), turns
DEBUG ON:
TRACE OFF \ make sure trace is off while defining trace action
: CHECK.FOR.ERROR ( -- )
THE.VARIABLE @
IF DEBUG ON
ENDIF ;
This word is then installed as the trace action by executing:
CFA.FOR CHECK.FOR.ERROR IS.TRACE.ACTION
Now the program is compiled with TRACE ON, and executed with DEBUG initially OFF so that the program runs at near full speed and no traced output is written to the screen:
TRACE ON
... compile buggy program here ...
DEBUG OFF
... execute buggy program
As soon as the bug manifests itself, the trace action word turns DEBUG ON which initiates the printout of all trace information. This shows us where the bug is occurring in the program.
Other Handy Debugging Words
Two kernel words are very useful during debugging sessions: PAUSE.ON.KEY and DUMP. PAUSE.ON.KEY, described earlier in this chapter, can be compiled into any word to allow you to suspend (with any keystroke) or ABORT (with a carriage return) its operation. This is especially useful when you are uncertain of how the word will function. For example, if you are uncertain as to whether a BEGIN … UNTIL loop in a word will terminate properly, put a PAUSE.ON.KEY command into the loop so that the word can be paused or aborted with a keystroke if something goes wrong.
DUMP expects an extended address under a count on the stack. It prints the contents of count bytes in memory starting at the specified address. The contents are printed in hexadecimal base regardless of the current number base, and the ascii equivalent contents are listed in a separate column. DUMP can be used to examine compiled code, check the contents of arrays, examine variables, and make sure that memory locations are being modified properly.
Floating point mathematics
Many applications require floating point mathematics. Unlike many small microcontrollers, the PDQ Board provides a built-in floating point package including trigonometric, logarithmic, and exponential functions and formatted real number input and output.
Programming in QED-Forth with floating point arithmetic confers many advantages. Fractional numbers are easy to represent, and the wide dynamic range of floating point numbers means that you don’t have to worry about the range limitations of integer math or the hassle of scaling operations to preserve precision. Floating point math is simple; instead of a host of integer, double number, and mixed-number operations, the four basic floating point operations F+ F- F* and F/ perform the math while optimally preserving precision. Error checking is supported, and floating point numbers can be recognized and printed in a variety of convenient formats. This section describes the floating point operations in detail.
Numbers that include either a decimal point or an embedded “E” or “e” are recognized by the QED-Forth interpreter as floating point numbers. Numbers without a decimal point or “E” are recognized as integers. It is strongly recommended that all floating point numbers be entered with a decimal point.
The following general floating point format is accepted:
Sxxx.yyyEszz
where
S
|
is the sign of the number, either “+”,” -”, or missing.
|
xxx.yyy
|
is the mantissa part of the number, any length, with an optional (but recommended) decimal point. Isolated embedded commas are permitted.
|
E ( or “e”)
|
designates that an optional base 10 exponent may follow.
|
s
|
is the sign of the base 10 exponent, either “+”,”-”, or missing.
|
zz
|
is the exponent, up to 2 digits.
|
Positive and negative numbers are represented over a magnitude range of 1.0x2^-126 to 1.99999x2^+127 corresponding to approximately 1.2x10^-38 to 3.4x10^38. The resolution of the mantissa is 6.9 decimal digits.
Use a Decimal Point When Entering a Floating Point Number
It is recommended that a decimal point always be included when entering a floating point number. If the current number base is hexadecimal, “E” is a valid digit, and a number containing an E but no decimal point could be interpreted as an integer.
As described in the previous chapter, when the QED-Forth interpreter gets a string from the input stream, it first tries to find it in the dictionary. If it is not found, it tries to convert it to an integer in the current number base and, if it cannot be converted, tries to convert it to a floating point number. An embedded decimal point in the numeric string prevents the string from being interpreted as a valid integer, but allows it to be interpreted as a floating point number (if the rest of the string is in the proper floating point format). Depending on the current number base, an embedded “E” may or may not be sufficient to flag the number as a floating point quantity.
Floating point numbers are always converted and printed as decimal numbers, irrespective of the contents of the user variable BASE.
Some examples of valid input numbers are:
.414 +1.414e
1234.56E11 1.5678E-23
-0.123e5 -.0E
-.00001E+1 5.
12,344. 1e
0. .1e-4
Printing Formats
Floating point numbers can be printed using any of the words
SCIENTIFIC.
FIXED.
FLOATING.
F.
All of these routines remove a floating point number from the stack and print it via the serial port. The first three of these routines print a number in the specified format (scientific, fixed, or floating, respectively). The word F. prints the number in the default format. After a cold restart, the FLOATING format is the default used by F., but the programmer can execute one of the words FIXED SCIENTIFIC or FLOATING to select which format is used by subsequent execution of F. .
Fixed Format
The fixed format is controlled by the user variables
LEFT.PLACES and
RIGHT.PLACES which set the maximum number of characters to the left and right of the decimal point. After a cold startup, these values are initialized to 4 and 3, respectively, and the programmer can alter them using the
! (store) command.
Other user variables that control the fixed format are the flags
TRAILING.ZEROS and
FILL.FIELD. If
TRAILING.ZEROS is
OFF, trailing zeros to the right of the decimal point are suppressed; this is the default state. To print all significant zeros up to a maximum of
RIGHT.PLACES digits, execute
TRAILING.ZEROS ON
To decimal align tabular output in fixed mode (as when you are printing out a matrix or table of values), execute
FILL.FIELD ON
which pads out each floating point number with spaces so that the decimal points line up in each column of data. The default state of FILL.FIELD is OFF.
Scientific Format
The SCIENTIFIC format prints a minus sign if needed, then one digit to the left of the decimal point, MANTISSA.PLACES digits to the right of the decimal point, followed by E, the sign of the exponent, and a 2-digit exponent. MANTISSA.PLACES is a user variable that can be altered by the programmer.
Floating Format
FLOATING format automatically chooses between fixed and scientific formats to display the number. The number is printed in a field size specified by the
SCIENTIFIC format, but the number is printed in
FIXED format if it can be displayed with at least as many significant digits as it would using scientific format. If not,
SCIENTIFIC format is used. Try playing with the format options to see how they work:
SCIENTIFIC↓ ok
45.678 F.↓ 4.5678E+01 ok
45.678 4 MANTISSA.PLACES ! F.↓ 4.5678E+01 ok
FIXED↓ ok
45.678 1 RIGHT.PLACES ! F.↓ 45.7 ok
45.678 3 RIGHT.PLACES ! F.↓ 45.678 ok
FLOATING↓ ok
45.678 1 MANTISSA.PLACES ! F.↓ 46. ok
45.678 4 MANTISSA.PLACES ! F.↓ 45.678 ok
Turning FILL.FIELD ON makes tabular output in FLOATING format neater by ensuring that the field width of numbers printed in scientific notation is the same as the width of numbers printed in fixed notation.
Stack notation and floating point stack operators
A floating point number uses two normal stack locations. A family of stack manipulation words is available to manipulate these 32-bit items, including:
FDROP F2DROP FSWAP
FROT FDUP F2DUP
FOVER FDUP>R F>R
FR> FR@ FR>DROP
In writing stack notation floating point numbers are identified with an “r” (for real numbers), while signed and unsigned integers are identified with an “n” and “u”, respectively (consult the glossary for a list of stack symbols).
Floating point operators
The floating point operators F+ F- F* and F/ perform floating point addition, subtraction, multiplication, and addition, respectively. Each requires two operands on the stack and places its result on the stack. A number of transcendental functions are also available for powering (F^N F** and 10^N), arithmetic inversion (FNEGATE), multiplicative inversion (1/F), absolute value (FABS), square root (FSQRT), scaling (F2* F2/ FSCALE), trigonometric, logarithmic, and exponential conversions.
Trigonometric Functions
The following trigonometric and inverse trigonometric functions are available:
FSIN FASIN
FCOS FACOS
FTAN FATAN
The trigonometric functions require their argument in radians and the inverse trigonometric functions return their results in radians. Degrees may be converted to radians using >RADIANS. For example:
45.0 >RADIANS FSIN F. ↓ 0.7071 ok
Similarly, the results of the inverse trigonometric functions may be converted to degrees using >DEGREES:
0.5 FSQRT FASIN >DEGREES F. ↓ 45. ok
Logarithmic and Exponential Functions
QED-Forth’s logarithmic functions are:
FLOG2
FLN
FLOG10
and exponential (“anti-log”) functions are:
FALOG2
FALN
FALOG10
For example,
200. FLOG10 F. ↓ 2.301 ok
2.301 FALOG10 F. ↓ 200. ok
The base two logarithm FLOG2, and exponential, FALOG2, are slightly faster than the natural logarithm, FLN, and exponential, FALN, or the base 10 log and exponential FLOG10 and FALOG10.
Random Number Generation
FRANDOM uses a pseudo-random bit sequence generator to leave a random number between 1.0 and 2.0 on the stack. The last 23-bit mantissa generated is stored in the user variable RANDOM#. This user variable can be initialized to create a reproducible sequence of pseudo-random numbers. RANDOM.GAUSSIAN generates a pseudo-random value drawn from a Gaussian distribution with unity standard deviation and zero mean.
Floating Point Comparison
The following words compare one or two floating point numbers on the stack and return a boolean flag:
F0= F0<> F0< F0> F0>= F0<=
F= F<> F< F> F>= F<=
In addition, there are fast operators for choosing the minimum or maximum of two values:
FMAX ( r1\r2 -- r | retains the most positive )
FMIN ( r1\r2 -- r | retains the most negative )
Pre-defined Floating Point Quantities
The following pre-defined words place floating point constants on the stack:
ZERO ONE TEN
PI PI/2 2PI/360 360/2PI
1/TEN 1/PI SQRT(2) 1/SQRT(2)
LOG10(2) LN(2) 1/LOG10(2) 1/LN(2)
INFINITY -INFINITY 1/INFINITY -1/INFINITY
The final row of constants lists the largest and smallest numbers that can be represented. In addition to setting the FP.ERROR flag, mathematical operations that overflow return a result of positive or negative infinity, and operations that underflow return positive or negative 1/infinity.
Floating point memory access, variables, and constants
The kernel words FCONSTANT and FVARIABLE define floating point constants and variables. For example, the square root of three may be defined as a floating point constant by executing
3.0 FSQRT FCONSTANT SQRT.OF.3 ↓ ok
Now execution of SQRT.OF.3 will place the square root of 3.0 on the stack:
SQRT.OF.3 F. ↓ 1.7321 ok
F@ and F! can be used with floating point variables just as @ and ! are used with integer variables. These are “page smart” memory operators that can access memory on any page, and correctly handle memory accesses that cross page boundaries.
Floating point self-fetching variables may be defined with the word REAL: as
REAL: BUDGET.DEFICIT↓
To set the value, use the TO operator as
4.0E12 TO BUDGET.DEFICIT↓
Stating the name of the self-fetching variable leaves the value on the stack:
BUDGET.DEFICIT F. ↓ 4.000E+12 ok
Number conversion
Integer to Floating Point Conversion
FLOT converts a signed integer on the stack to a floating point number on the stack. For example,
-1234 FLOT F. ↓ -1234. ok
-1234 FLOT 10000 FLOT F/ F.↓ -0.1234 ok
Note that FLOT converts integers greater than 32,767 to negative floating point numbers. Use UFLOT to convert an unsigned integer on the stack to a floating point number. For example,
60000 UFLOT F. ↓ 60000. ok
DFLOT converts a signed double number to a floating point number. For example,
DIN 123400 DFLOT F. ↓ 1.234E+05 ok
Floating Point to Integer Conversion
The kernel word
INT.PART converts a signed floating point number on the stack to an integer, truncating the fractional part toward zero. For example,
35.6 INT.PART . ↓ 35 ok
-12.7 INT.PART . ↓ -12 ok
Likewise, DINT converts a signed floating point number to a double number, as
1.234E5 DINT D. ↓ 123400 ok
FIXX rounds a signed floating point number to the nearest integer. For example,
1.45 FIXX . ↓ 1 ok
-4.8 FIXX . ↓ -5 ok
Note that FIXX converts floating point numbers greater than 32,767 to the most positive signed 16-bit integer which is +32,767. Use UFIXX to convert an unsigned floating point number up to 65535 to a floating point number. For example,
60000. UFIXX U.↓ 60000 ok
DFIXX rounds a signed floating point number to the nearest double number. For example,
70,000. DFIXX D.↓ 70000 ok
INT.FLOOR returns the most positive signed integer less than or equal to a floating point number. For example,
4.99 INT.FLOOR .↓ 4 ok
-3.99 INT.FLOOR .↓ -4 ok
-2. INT.FLOOR .↓ -2 ok
Likewise, DINT.FLOOR returns the most positive signed double number less than or equal to a floating point number.
Floating Point to Integer to Floating Point Conversion
FINT is just like
INT.PART but it returns a floating point number–it’s the equivalent of
INT FLOT. For example,
35.6 FINT F.↓ 35. ok
FRTI (f-round-to-integer) rounds a floating point number to the nearest signed integer, just as FIXX does, but leaves it in floating point representation. It is the equivalent of FIXX FLOT. For example,
0.45 FRTI F.↓ 0. ok
0.55 FRTI F.↓ 1. ok
-4.8 FRTI F.↓ -5. ok
Likewise, FLOOR is a version of INT.FLOOR that returns the result in floating point format.
Floating Point to String Conversion
F>FIXED$ (pronounced “f-to-fixed-string”),
F>SCIENTIFIC$ and
F>FLOATING$ each convert a floating point number on the stack to a string in fixed, scientific or floating format respectively. These formats are explained in a prior section of this chapter. The address of the string is returned under the count under a true flag. For example, we could define
: TYPE.FIXED ( r -- ) \ types r in fixed format
F>FIXED$
IF COUNT.TYPE
ENDIF
;
PI TYPE.FIXED↓ 3.142 ok
If conversion is unsuccessful (for example, as a result of LEFT.PLACES and RIGHT.PLACES being too small to accommodate the number in FIXED format) a false flag is returned.
String to Floating Point Conversion
FNUMBER expects the extended address of the string on the stack, and converts the string to a floating point number which is left on the stack under a flag. The string can have any number of leading spaces but it must be terminated with a space. For example, try
“ .01234e+3 “ FNUMBER . F.↓ -1 12.34 ok
Numeric Input
NEXT.NUMBER may be used to input a floating point number from the keyboard. It ignores any non-numeric inputs preceding the first valid number, and waits for as many lines as necessary for the first valid number. The first valid integer or floating point number is left on the stack as a floating point number (integers are converted to floating point via the FLOT operator). For example,
NEXT.NUMBER some garbage 12 F.↓ 12. ok
ASK.FNUMBER waits for a line of text terminated by a carriage return, and attempts to convert the text to a valid floating point number. Consult its glossary listing for details of operation.
Floating point error handling
The user variable FP.ERROR holds an error flag that can be inspected after any floating point operation. If the contents are 0, there was no error, while contents of 1 indicate underflow and -1 indicates overflow. The FORTH words UNDERFLOW and OVERFLOW set FP.ERROR to 1 and -1, respectively. In an application in which errors must be detected, the FP.ERROR flag must be polled before the next floating point operation changes them.
Errors detected in the floating point math operations do not cause an ABORT. If the programmer desires, this can be accomplished by polling the FP.ERROR variable and executing an ABORT with an appropriate message if it is non-zero. For example, F/ could be redefined to halt processing if an error is detected during division:
: F/ ( r1\r2 -- r3 | r3 = r1/r2 )
F/ FP.ERROR @ ?DUP
IF \ if error occurred
-1 =
IF .” Overflow or divide by zero error!”
ELSE .” Underflow error!”
ENDIF \ print message...
ABORT \ ... and abort
ENDIF
;
The heap memory manager
QED-Forth provides a heap memory management system. QED-Forth’s array and matrix math routines make use of the heap memory manager, but they do it transparently. To use these features you need only a cursory familiarity with the heap. On the other hand, if you want to create your own sophisticated dynamically allocated data structures, you should read this section carefully.
A “heap” is a pool of memory from which smaller blocks of memory of variable size are allocated. The heap manager allocates blocks of memory requested by the user’s program from this pool. When the program is finished with the block of memory, it can return it to the heap. This results in efficient use of memory, especially for data structures which can vary in size and for temporary data structures which are used and then de-allocated. The de-allocated memory becomes available for use by other segments of the program. QED-Forth can maintain any number of heaps simultaneously, each containing numerous data structures.
Heap Compacting
The heap manager works by allocating blocks of memory beginning at the designated starting address of the heap. When a heap item is de-allocated and returned to the heap, its memory is free for use by other programs. A simple-minded heap manager which simply keeps track of which areas of the heap are used and which are free encounters the problem of heap fragmentation. For example, if heap items #1, #2, and #3 are sequentially allotted memory, they will occupy blocks of memory starting at the bottom of the heap. Then if item #2 is de-allocated, the heap’s free memory will consist of two disjoint sections: the memory that was occupied by item #2, and the memory above item #3. As this process of allocation and de-allocation continues, the heap gets fragmented into smaller available pieces so that less of the heap consists of contiguous memory. This makes it difficult or impossible for the heap manager to respond to requests for large blocks of contiguous memory.
To solve this problem, the QED-Forth heap manager “compacts the heap” each time a heap item is de-allocated. This involves re-arranging the blocks in the heap so that all of the free heap memory is available in one contiguous block above the allocated heap items.
Handles to Heap Items
The problem then becomes one of keeping track of where the items are in the heap. The process of compaction moves the heap items so that the base addresses of the heap items change. But the program that requests a heap allocation must have a reliable address that can be used to refer to the heap item. The heap manager solves this problem by adding a level of indirection in the addressing. Instead of informing the requesting program of the actual base address of the heap item when it is allocated, the heap manager returns to the requesting program a “handle” (also called an “xhandle”) which is an extended address that contains the extended base address of the heap item. As long as a particular heap item is allocated, its handle does not change and can be used by the requesting program. The heap manager changes the handle’s contents appropriately as the heap is compacted, and the requesting program can fetch the contents of the handle at any time to obtain a valid base xaddress for the heap item.
Thus the problems of heap fragmentation and the changing of base addresses are solved by compacting the heap and using handles instead of direct addresses.
Heap Size
In QED-Forth, the maximum size of a heap is limited only by the amount of available contiguous
RAM. A heap can flow over page boundaries. Likewise, the size of a data structure in the heap is limited only by the available memory in the heap.
The only heap limitation is that the list of handles which is maintained near the top of the heap must be on the last page of the heap, and the handle list cannot flow over a page boundary. Because each handle requires only 4 bytes, this poses very little limitation for most practical applications.
The following implementation details clarify how the heap works. The next section discusses how array and matrix data structures use the heap to facilitate dynamic dimensioning and memory allocation.
Initializing the Heap
To create a heap, specify a starting extended address and an ending extended address and execute
IS.HEAP. For example, the 80 Kbyte heap that is set up by a
COLD' restart or by executing
DEFAULT.MAP' extends from 0x8000 on page 0x18 through address 0xBFFF on page 0x1C. This heap is declared and initialized by executing
HEX 8000 18 BFFF 1C IS.HEAP↓ ok
This command sets the user variable CURRENT.HEAP' in the user area to 0xBFFF on page 0x1C, and initializes the heap management variables on the heap page to indicate that START.HEAP' is at 0x8000 on page 0x18, and there are no allocated heap items. The hexadecimal number base is more convenient than decimal for specifying memory map address locations, so it will be used for the remainder of the section.
The user variable called CURRENT.HEAP' contains an extended address that specifies the address and page of the top byte + 1 in the heap. The heap allocation begins in low memory at the extended address pointed to by START.HEAP; a variable named HEAP.PTR' holds the extended address of the next byte available for allocation. The contents of START.HEAP' and HEAP.PTR' as well as 2 other variables that manage the heap (HANDLE.PTR' and FREE.HANDLE) are kept in the heap area, just below the specified end of the heap. The handles are kept below these variables in the heap.
The kernel word ROOM calculates the amount of space available in the current heap and returns it as a 32-bit number on the stack:
ROOM D.↓ 13FE7 ok
which is nearly 80 Kbytes, as expected. When all available heap memory has been allocated and no more items can be added to the heap, ROOM returns 0\0.
Allocating Heap Items
To request a block of 1FEH bytes from the heap execute
DIN 1FE FROM.HEAP↓ ok [ 2 ] \ BFEF \ 1C
which expects a double number size in bytes and returns the extended address of the handle for the heap item. A non-zero handle indicates that the heap manager successfully allocated the memory. The requesting program should save this handle because its contents contain the base address needed to access the data in the heap item. It can be saved in a self-fetching variable whose name corresponds to the function of the heap item. For example,
XADDR: 1ST.DATA.BLOCK↓ ok [ 2 ] \ BFEF \ 1C
TO 1ST.DATA.BLOCK↓ ok \ save the handle in the variable
A handle of 0\0 is returned if there is not enough memory in the heap to allocate the item. None of the heap manager words ABORT' if an error is detected; rather, they signal the error by passing a zero handle or an error flag to the calling program. It is the calling program’s responsibility to take the appropriate response in case of an error.
If the requested size passed to FROM.HEAP' is not an even multiple of 4 bytes, the heap manager rounds the size up to the next 4-byte multiple. In this case, the size is rounded up to 0x200 which is 2 bytes more than the requested size. In addition, the heap manager ensures that the base address of each heap item is an even multiple of 4 bytes. These steps speed heap compaction.
To obtain the base address of the heap item, fetch the extended base address from the handle:
1ST.DATA.BLOCK X@↓ ok ( 2 ) \ 8004 \ 18
SP!↓ ok \ clear the data stack
The contents, 0x8004 on page 0x18, specify the base address of the heap item at this time. The heap manager saves the 32-bit size of the heap item in the four bytes below the base address. The word ?HANDLE.SIZE expects the extended handle address on the stack and returns the 32-bit size of the heap item:
1ST.DATA.BLOCK ?HANDLE.SIZE D.↓ 200 ok
which prints 0x200 as expected.
The word .HANDLES prints the status of the current heap:
.HANDLES ↓
Size Addr Handle
200 8004\18 BFEF\1C
ok
This shows that one handle (0xBFEF on page 0x1C) has been allocated with current base address 0x8004 on page 0x18, with a size of 0x200 bytes. .HANDLES always prints in hexadecimal base, regardless of the current number conversion BASE.
Another heap item can be allocated as
XADDR: 2ND.DATA.BLOCK↓ ok
DIN 400 FROM.HEAP↓ ok [ 2 ] \ BFEB \ 1C
TO 2ND.DATA.BLOCK↓ ok
The heap manager returns a handle 0xBFEB on page 0x1C and again the allocation is successful.
De-allocating Heap Items
To de-allocate the first heap item heap item execute
1ST.DATA.BLOCK TO.HEAP .↓ FFFF ok
TO.HEAP expects a handle xaddress on the stack, and returns a success flag. A true flag indicates that the handle is valid and that the heap item has been successfully de-allocated, and a false flag indicates that the handle was invalid and no action was taken. Execute .HANDLES to see the effect of the de-allocation:
.HANDLES ↓
Size Addr Handle
400 8004\18 BFEB\1C
* 0\ 0 BFEF\1C
ok
The handle of 1ST.DATA.BLOCK that was returned to the heap is shown with an asterisk to indicate that it is not in use (because it has been de-allocated) and that the listed base address and size are not significant. The handle of 2ND.DATA.BLOCK has not changed, but its base address has been changed to 0xB004 on page 0x1C. It was moved to the bottom of the heap during the heap compaction that occurred when 1ST.DATA.BLOCK was de-allocated. The heap manager moved the memory and adjusted the contents of the handle to implement the heap compaction. All of the available memory is maintained as one contiguous block above the last allocated heap item.
Maximizing the Efficiency of Heap Compaction
Heap compaction occurs each time an item is returned to the heap. All of the bytes above the de-allocated item are moved down in memory. This move requires 2 microseconds per byte, or 2 milliseconds per Kbyte.
Notice, however, that compacting the heap to de-allocate the last item allocated is accomplished by simply adjusting the heap pointer; there is no need to move a block of memory in the heap. When possible, programs should de-allocate heap items in the proper order so that the last item allocated is the first item de-allocated; this maximizes execution speed. For example, when allocating several temporary matrices to hold the intermediate results of a calculation, de-allocating them in the proper order improves performance.
Resizing and Copying Heap Items
RESIZE.HANDLE expects a valid 32-bit handle under a 32-bit number of bytes on the stack, and tries to resize the heap item to the specified size, preserving as much of the item’s data as possible. The heap must have enough room to copy the heap item. A flag is returned to indicate whether the resizing was successful.
DUP.HEAP.ITEM expects a handle xaddress on the stack, and creates a copy of the specified item in the current heap. The copy’s handle is returned if the duplication was successful; otherwise 0\0 is returned. For example, to duplicate 2ND.DATA.BLOCK, execute
XADDR: 2ND.COPY↓ ok \ create a name for the new heap item
2ND.DATA.BLOCK DUP.HEAP.ITEM↓ ok [ 2 ] \ BFEF \ 1C
TO 2ND.COPY↓ ok \ save the handle
To verify that the heap item has been duplicated, execute .HANDLES:
.HANDLES ↓
Size Addr Handle
400 8004\18 BFEB\1C
400 8408\18 BFEF\1C
ok
The heap manager has re-used the handle from the recently de-allocated word 1ST.DATA.BLOCK and assigned it to the newly allocated item.
Multiple Heaps
In a timesliced multitasking environment where several tasks are using heap memory, each task must have its own separate heap. If tasks share a single heap, an interrupting task could compact the heap and change the contents of a handle that is about to be used by another task. Thus multitasking environments may require multiple heaps. There may be other cases where it is advantageous for a single task to have multiple heaps.
To use multiple heaps, the programmer must manage the user variable
CURRENT.HEAP. To see how this works, let’s set up a second heap with a size of 8 Kbytes that starts on page 0x16 at address 0xB000 and ends on page 0x17 at 0x9000.
Recall that we are in HEX base. Now execute:
B000 16 9000 17 IS.HEAP↓ ok \ set up new heap
XADDR: 1ST.HEAP↓ ok
XADDR: 2ND.HEAP↓ ok \ define save variables for CURRENT.HEAP
BFFF 1C TO 1ST.HEAP↓ ok
9000 17 TO 2ND.HEAP↓ ok \ initialize the save variables
The self-fetching variables 1ST.HEAP and 2ND.HEAP hold the values of CURRENT.HEAP that specify the two heaps. Recall that the contents of the 32-bit user variable CURRENT.HEAP specifies the heap in which heap items are to be allocated and de-allocated. Thus manipulating the single extended address in CURRENT.HEAP enables multiple heaps to be managed. For example, to make the heap on page 0x1C the current heap, execute
1ST.HEAP CURRENT.HEAP X!↓
and to make the heap on pages 0x16 and 0x17 the current heap, execute
2ND.HEAP CURRENT.HEAP X!↓
Transferring Heap Items
TRANSFER.HEAP.ITEM copies a heap item into any specified heap on any page. For example, to transfer a heap item from the 1ST.HEAP into 2ND.HEAP, put the handle of the source heap item and the destination
CURRENT.HEAP on the stack and execute
TRANSFER.HEAP.ITEM as
XADDR: 3RD.DATA.BLOCK↓ ok
2ND.DATA.BLOCK 2ND.HEAP TRANSFER.HEAP.ITEM↓ ok [ 2 ] \ 8FF0 \ 17
TO 3RD.DATA.BLOCK↓ ok \ save the new handle
The new item resides in 2ND.HEAP and its handle is saved in 3RD.DATA.BLOCK. The transfer was successful; otherwise, TRANSFER.HEAP.ITEM would have returned a handle of 0\0.
To verify the status of 2ND.HEAP, execute
2ND.HEAP CURRENT.HEAP X!↓
.HANDLES ↓
Size Addr Handle
400 B004\16 8FF0\17
ok
which shows that the heap item was copied to 2ND.HEAP.
Heap-based data structures
Data structures that are maintained in the heap are usually created by “defining words” such as ARRAY: and MATRIX:. These defining words create and initialize a parameter field in the variable area, as described in the next section. The contents of the parameter field are filled in when the data structure is dimensioned or allocated. When the data structure is dimensioned it is assigned to the heap specified by CURRENT.HEAP. At this same time the dimensioning information (e.g., number of rows and columns), the handle (which contains the base xaddress of the data) and the value of CURRENT.HEAP are saved in the parameter field associated with the data structure. Based on the information in the parameter field, the memory manager can resize, copy, or de-allocate the heap item irrespective of the current contents of the user variable CURRENT.HEAP.
For example, the word DELETED which de-allocates arrays and matrices automatically saves CURRENT.HEAP, looks at the parameter field of the item to be deleted, puts its heap specification into the user variable CURRENT.HEAP, executes TO.HEAP to de-allocate the item, and restores the original value of CURRENT.HEAP. Thus arrays and matrices in any heap can be deleted. Other array and matrix operations perform similar user-transparent manipulations of CURRENT.HEAP to make the words powerful and remove book-keeping chores from the programmer.
Because both the heap and the variable area where the parameter field is stored are in modifiable memory (RAM), the data structure can be allocated, re-dimensioned, or de-allocated “on the fly” as the program executes. This is a powerful programming capability. It is the basis for the comprehensive array and matrix package which is discussed in the next section.
Arrays and matrices
QED-Forth provides a set of data structures and associated operations that simplify and streamline sophisticated processing tasks. Arrays and matrices can be easily created, dynamically dimensioned, deleted, initialized, and copied. Matrices are defined as 2-dimensional arrays of floating point numbers. This chapter describes how to define and use these data structures.
Arrays
An array is a data structure stored in memory whose elements can be individually addressed and accessed. QED-Forth supports dynamically dimensioned arrays whose elements are stored in heap. The array can be redimensioned as needed under program control and the array’s storage space can be dynamically re-allocated. Consult the “Array” section of the Categorized Word List in the QED-Forth Glossary for a list of available routines in the array library.
Array Size and Dimensions
The number of elements per dimension and the number of bytes per element must be between 1 and 65,535. An array can have 1 or 2 dimensions. The size of an array is limited by the amount of space available in the current heap. The size of the heap, in turn, is limited only by the amount of available contiguous RAM. The heap may occupy multiple contiguous pages.
Creating Arrays
To create a named array execute ARRAY: followed by a name of your choice, as
ARRAY: ARRAY.NAME↓
The array is created but no heap memory is assigned to it yet. Memory is allocated and the array is dimensioned using the command DIMENSIONED which requires the limits of the indices under the number of indices under the number of bytes per element under the array’s extended parameter field address (xpfa). There can be either 1 or 2 indices. For example to dimension an already named array to have 2 dimensions, 5 rows and 10 columns, with 2 bytes per element, execute
DECIMAL↓ \ set base to decimal
5 10 2 2 ‘ ARRAY.NAME DIMENSIONED↓
where ‘ (pronounced “tick”) places the extended parameter field address on the stack; the xpfa is used to refer to the array as a whole. Memory for the array is allocated in the current heap. If the heap has insufficient room for the specified array, QED-Forth will ABORT and print the message:
ARRAY.NAME is too large. Not enough memory !
Addressing Array Elements
Individual elements are referred to by placing the element’s indices on the stack and stating the array’s name; the element’s extended address is then left on the stack. The element size and all indices are 16-bit unsigned integers, and indices are zero-based. This means that for a two dimensional array with 5 rows and 10 columns the rows are numbered 0 through 4 and the columns are numbered 0 through 9. To store the integer 104 at the second row (row#1) and seventh column (column#6) you can say
104 1 6 ARRAY.NAME !↓ ok
and to fetch and print the value from the same location you can execute,
1 6 ARRAY.NAME @ .↓ 104 ok
The indices are checked to insure that they are within their limits.
Out-of-range indices cause an ABORT and an error message. For example,
5 6 ARRAY.NAME @ ↓
Error at [] ARRAY_NAM_ 5 is an out-of-range index ! ok
The error message reports the routine that detected the error (which is [], a word that calculates an address given the indices), the name of the array, the offending index, and the reason for the ABORT. Note that only the first 9 letters of the array’s name were printed. This is because we assume that the user variable WIDTH equals its default value of 9. The remainder of the name is represented by the underbar character. (To improve the reporting of names in error messages, you can increase WIDTH with a simple ! command).
References to Entire Arrays
An entire array can be referred to, as opposed to referring to only an individual element, by using the kernel word ‘ (tick) before the name. Executing ‘ followed by the array’s name leaves the 32-bit extended parameter field address (xpfa) of the array on the stack; this address must be used whenever an entire array is treated as a single object. It is useful when you need to pass an entire array to a FORTH word which then operates on all elements of the array rather than on just a single element. References to arrays can be passed from one word to another without the called word needing prior knowledge of the array’s name.
Operations for Entire Arrays
An entire array can be initialized to zero as
‘ ARRAY.NAME ZERO.ARRAY↓
Suppose a character array is needed to hold a character string. To define and dimension a 1-dimensional array with 80 single-byte elements, execute
ARRAY: CHARACTER.ARRAY 80 1 1 ‘ CHARACTER.ARRAY DIMENSIONED↓
and then initialize it to contain ascii blanks as
‘ CHARACTER.ARRAY BLANK.ARRAY↓
The array can be initialized to contain any specified character using FILL.ARRAY. For example, to initialize an array to hold ascii zeros (whose code is decimal 48), execute
‘ CHARACTER.ARRAY ASCII 0 FILL.ARRAY↓
Try fetching and printing the first element of the array to verify that the operation worked:
0 CHARACTER.ARRAY C@ .↓ 48 ok
Note that C@ was used to retrieve a single byte from the array; @ would have retrieved two successive bytes.
The word SWAP.ARRAYS expects the xpfa’s of two arrays on the stack. It swaps the contents of the parameter fields of the arrays (which contain dimensioning information and an indirect pointer to the contents) without moving the contents in the heap. Thus the swap is accomplished very rapidly.
The contents of one array can be copied to another array, irrespective of the prior dimensions or contents of the destination array, using COPY.ARRAY. For example, to copy ARRAY.NAME
to CHARACTER.ARRAY
, execute
‘ ARRAY.NAME ‘ CHARACTER.ARRAY COPY.ARRAY↓
Deleting an array de-allocates its memory in heap, undimensions it by clearing its parameter field, and leaves only its name remaining. It is accomplished by executing
‘ CHARACTER.ARRAY DELETED↓
Determining a Pre-existing Array’s Dimensions
?ARRAY.SIZE returns the 32-bit number of elements (not number of bytes!) of an array:
‘ ARRAY.NAME ?ARRAY.SIZE D.↓ 50 ok
and the limits of all its indices are returned by
‘ ARRAY.NAME ?DIMENSIONS↓ [ 4 ] \ 5 \ 10 \ 2 \ 2 ok
which are the dimensions we specified. The stack may be cleared using
SP!↓ ok
Internal Representation of Arrays
This section is provided for those interested in understanding the definitions of array defining words. You need not read or understand this section to use arrays.
An array (or a matrix) has four parts:
Name: Stored in the names area of the dictionary.
Action: Performed by the FORTH word [] which is automatically called when the array’s name is executed with indices on the stack; the extended address of the specified array element is placed on the stack. Error checking is performed to see if any index exceeds its limit.
Parameter field: Holds the limits for each of the array indices, the number of dimensions, the element size, a handle to the array’s storage space in the heap, and a reference to CURRENT.HEAP. The parameter field is stored in the variable area in RAM.
Contents: Stored in the heap as a relocatable block of memory.
After an array name is created, the array dimensions (index limits) and element size are stored in the parameter field and the array elements are stored in the heap.
The contents of the sixteen-byte parameter field are as follows:
Table 16-1 Contents of the Parameter Field of an Array or Matrix. |
---|
Address | Description |
---|
xpfa +0 | 32-bit extended handle to the array’s contents in heap |
xpfa +4 | addr of CURRENT.HEAP (page is same as handle’s) |
xpfa +6 | size in bytes of each element |
xpfa +8 | number of dimensions; 2 is maximum allowed |
xpfa +10 | limit of #columns |
xpfa +12 | limit of #rows |
xpfa +14 | handler flag |
If the handle is zero then the array is undimensioned. The element size and all indices are 16-bit unsigned integers. Indices are all zero-based. The command
ARRAY: <name>
creates a dictionary entry for <name> and initializes its dimensions to 0. It does not allocate heap space. Memory is allocated when arrays are dimensioned or redimensioned using DIMENSIONED. For example:
#rows #columns #dimensions element.size ‘ <name> DIMENSIONED
allocates sufficient memory from the heap for the array and stores the handle and dimensions in the parameter field of <name>. In this example, #dimensions must equal 2 because both rows and columns are specified. Any prior contents of the array are deleted. The command
‘ <name> DELETED
de-allocates memory for the array and returns its handle to the heap manager.
Note that executing an X@ from the xpfa of the array returns the extended address of the handle to the heap item; executing an X@ from this handle returns the base xaddress of the heap item. As discussed in the heap section, the handle remains valid as long as the array is dimensioned, but the base xaddress (the contents of the handle) may change every time the heap is compacted. The array words take care of address calculation for you so you don’t have to perform explicit memory fetches to find the address of an array element. Just place the indices on the stack and state the array’s name to obtain an element’s address.
Matrices
A matrix is a two-dimensional array of floating point numbers. Matrix defining words work analogously to array defining words except that the number of dimensions (2) and element size (4 bytes) are fixed and need not be explicitly stated.
Creating, Dimensioning, and Addressing Matrices
To create a matrix use MATRIX: followed by the desired name. For example, to create a matrix called MATRIX.NAME execute
MATRIX: MATRIX.NAME↓
This command creates the matrix but does not assign dimensions or allot memory to it. It can be dimensioned using DIMMED. For example to dimension a matrix with 5 rows and 10 columns execute
5 10 ‘ MATRIX.NAME DIMMED ↓
which allocates memory from the heap and sets up the parameter field to specify the number of dimensions. At this point the matrix elements are not initialized.
The useful word ?DIM.MATRIX expects the xpfa of a dimensioned matrix on the stack, and returns the number of rows under the number of columns for the specified matrix:
‘ MATRIX.NAME ?DIM.MATRIX↓ ok ( 2 ) \ 5 \ 10
?DIM.MATRIX aborts with an error message if the specified xpfa is not associated with a dimensioned matrix. Thus it provides error checking as well as returning the dimensions of the matrix.
Floating point matrices that consist of a single row or a single column are still assigned two dimensions. For example, a column vector with N rows is dimensioned as an Nx1 (pronounced “n-by-one”) matrix, and a row vector with M columns is dimensioned as an 1xM matrix. This keeps the matrix notation standard, and allows row vectors and column vectors to be distinguished from one another; this is important for certain matrix math operations.
Placing the row and column indices on the stack and invoking the name of the matrix leaves the extended address of the matrix element on the stack. The word M[], which is automatically called when the matrix name is executed, performs the address calculation. Given the element address, the standard floating point store and fetch operators F! and F@ can be used to access the element. For example, to store the floating point number 15.6 at the second row (row#1) and fifth column
column#4), execute
15.6 1 4 MATRIX.NAME F!↓ ok
and to fetch and print the number execute
1 4 MATRIX.NAME F@ F.↓ 15.6 ok
Storing and fetching floating point values can also be accomplished with the words M[]! and M[]@, as
34.5 2 1 ‘ MATRIX.NAME M[]!↓ ok
2 1 ‘ MATRIX.NAME M[]@ F.* 34.5 ok
Another useful word is [0] which converts a matrix xpfa on the stack into the address of the first element. Its advantage is that using ‘ MATRIX.NAME [0] is faster than using either of these commands:
0 0 MATRIX.NAME
0 0 ‘ MATRIX.NAME M[] .
Matrix Initialization
After being dimensioned, a matrix can be initialized to contain all zeros by executing
‘ MATRIX.NAME ZERO.MATRIX↓
The word MATRIX can be used to enter a set of data values into a dimensioned matrix. For example, if we re-dimension MATRIX.NAME
as a 3 by 2 (that is, a matrix with 3 rows and 2 columns) as
3 2 ‘ MATRIX.NAME DIMMED↓ ok
we can then enter 6 data points as
MATRIX MATRIX.NAME = 1.5 2.5 3 4 5 6.45↓ ok
Note that either floating point or integer values can be entered; the integer values are automatically converted to floating point numbers before being stored into the matrix.
Matrix Display
To display the contents of a matrix, use M.. as
‘ MATRIX.NAME M.. ↓
Matrix Matr_______ =
1.5 2.5
3. 4.
5. 6.45
ok
Note that QED-Forth’s response is a valid initialization command; even the “ok” prompt is defined as a do-nothing word, so the M.. display format is the same as that required by the QED-Forth initialization word MATRIX . The command M. displays the contents of the matrix without printing the name of the matrix.
Copying and Swapping Matrices
A matrix can be copied to a destination matrix as
‘ SOURCE.MATRIX ‘ DESTINATION.MATRIX COPY.MATRIX
The destination matrix need not be dimensioned in advance; COPY.MATRIX dimensions it properly.
SWAP.MATRIX interchanges the contents of the parameter fields of two matrices to rapidly swap the matrices without moving their contents in heap.
An Example: Transposing a Matrix
The transpose AT of a matrix A is the matrix whose rows are the columns of A and whose columns are the rows of A. The following definition of MAT.TRANSPOSE finds the transpose of any input matrix, under the condition that the destination must be different from the source matrix. This definition is presented as an example of the usage of the matrix manipulation words.
: MAT.TRANSPOSE ( src.xpfa\dest.xpfa -- )
\ places the transpose of the source in the destination;
\ source and destination must be different matrices
XOVER ?DIM.MATRIX ( src.xpfa\dest.xpfa\#src.rows\#src.cols -- )
LOCALS{ &#cols &#rows x&dest.pfa x&src.pfa }
&#cols &#rows x&dest.pfa DIMMED \ dimension dest, reversing rows, cols
&#rows 0
DO \ for each source row
&#cols 0
DO \ for each source column
J I x&src.pfa M[] F@ \ fetch source element...
I J x&dest.pfa M[] F! \ ...and store in destination element
LOOP
LOOP
;
MAT.TRANSPOSE takes references to the source matrix and the destination matrix from the stack. It finds the dimensions of the source matrix, and loads the extended parameter field addresses of the source and destination as well as the number of rows and columns into local variables. Using local variables minimizes stack manipulations and makes the code easier to read. The number of rows and columns in the source matrix are swapped and used to dimension the destination matrix. A pair of nested DO LOOPs is used to fetch each element of the source array and copy it to the appropriate location in the destination array.
Note how the outer loop index J and the inner loop index I are used to specify the row and column numbers. Because MAT.TRANSPOSE doesn’t know the names of the passed matrices, it can not determine matrix element addresses just by invoking the matrix name with indices on the stack. Instead, it uses the word M[] which expects a matrix xpfa on the stack over the indices and returns the element address.
Structures
A “structure” is an object that groups different types of data according to a template designed by the programmer, and allows the programmer to designate names that can be used to store and retrieve the data items in the structure. While arrays hold data that is all one size and refer to the data elements using index numbers, structures let you group data of different sizes and refer to them using names of your choice. The structure defining words declare the type of the data (integer, real, address, extended address, etc.) and this enhances the clarity of the program.
Using structures involves defining them, instantiating them, and addressing their elements which are called “members”. Defining a structure can be thought of as setting up a template for the data. The template comprises a set of named offsets that specify the position of each member relative to the base of the structure. Instantiating a structure creates an instance of the structure and assigns it to a particular named memory location, either in the variable area, the definitions area of the dictionary, or in the heap. Addressing a member in a structure is accomplished by stating the name of the structure instance followed by the name of the structure member. Executing the name of the structure instance leaves its base xaddress on the stack, and then executing the name of the structure member automatically adds an offset to the base xaddress to produce the extended address of the member. Standard fetch and store operations can then be used to access the member.
Defining Structures
To define a structure, execute
STRUCTURE.BEGIN: followed by the name of the structure. Then state the appropriate member defining commands followed by member names that you choose.
STRUCTURE.END finishes the definition.
For example, the following commands create a structure to hold customer information:
STRUCTURE.BEGIN: CUSTOMER.INFO \ name the structure as a whole
INT-> +ACCOUNT# \ now name each of the members
40 STRING-> +CUSTOMER.NAME
50 STRING-> +STREET
35 STRING-> +CITY&STATE
DOUBLE-> +ZIP.CODE
REAL-> +ACCT.BALANCE
STRUCTURE.END \ end the structure definition
This creates a template for the data. If you try this example on your computer, you will notice that while the structure is being compiled, items are temporarily placed on the data stack by the compiler. These stack items must not be altered; they are used to initialize the size of the structure and the offset of the members.
The member defining word INT-> removes the next name from the input stream, +ACCOUNT#, and creates a header in the name area of the dictionary. The action of +ACCOUNT# is to add an offset to an extended address on the top of the stack. Because +ACCOUNT# is the first member in the structure, it adds an offset of 0. (Actually, members with offsets of 0 are smart enough to do nothing at run time, thereby saving execution time). Note that the member defining words end with → to suggest that they are defining the name that follows.
Note also that we start each member name with + to suggest that it adds an offset to the quantity on the stack. The defining word STRING→ creates a header and action for CUSTOMER.NAME and, based on the quantity on the stack, reserves space in the structure for the string. It reserves 1 byte more than the number on the stack to allow for a count byte at the start of the string. In the example structure two more string members are defined, then a double number field is reserved for the zip code, and a floating point field is reserved for the customer’s account balance.
Executing CUSTOMER.INFO leaves the size of the entire structure (in bytes) on the stack; this structure takes 138 bytes.
Here is a list of the available member defining words:
n RESERVED reserves unnamed space within a structure of n bytes in size
n MEMBER-> defines a member word with n bytes reserved
BYTE-> defines a single byte item
n BYTES-> defines an item of n bytes in size
INT-> defines a 2-byte (16-bit) integer
n INTS-> defines n 2-byte integer numbers
DOUBLE-> defines a 4-byte double number
n DOUBLES-> defines a collection of n 4-byte double numbers
REAL-> defines a 4-byte floating point number
n REALS-> defines a collection of n 4-byte floating point numbers
ADDR-> defines a 2-byte (16-bit) address
n ADDRS-> defines n 2-byte addresses
PAGE-> defines a 2-byte page, only the lower order byte is significant
XADDR-> defines a 4-byte extended address
n XADDRS-> defines a collection of n 4-byte extended address
XHNDL-> defines a 4-byte xaddr whose contents are a handle
n STRING-> defines a counted string n+1 bytes long, n <= 255
n STRUCT-> defines a member word with n bytes reserved
n1 n2 STRUCTS-> defines a collection of n1 structures each n2 bytes in size
Most of these words call the kernel word FIELD which creates a named offset that, when executed, calls XU+ to add the offset to the extended address on the top of the stack.
Creating Instances of Structures
To create a structure instance in the variable area for a particular customer named BURGER.WORLD, use
V.INSTANCE: as
CUSTOMER.INFO V.INSTANCE: BURGER.WORLD↓
CUSTOMER.INFO leaves the size of the structure on the stack. V.INSTANCE: (where the “V” indicates the variable area) reserves room in the variable area for the structure. It creates a definition for BURGER.WORLD that, when executed, leaves the extended address of the structure instance on the stack.
To initialize the account number in BURGER.WORLD to 1234, we can execute
1234 BURGER.WORLD +ACCOUNT# !↓
BURGER.WORLD leaves the extended address of the structure instance on the stack, +ACCOUNT# adds its offset to yield the extended address of the member, and ! saves the value 1234 into the specified member. Similarly, CMOVE can be used to initialize the name, street, and city strings, 2! can initialize the double number zip code, and F! can set the floating point account balance.
If we want a structure to eventually reside in write-protected memory in the definitions area instead of in the RAM variable area, we execute
CUSTOMER.INFO D.INSTANCE: MARTHA’S.PIES↓
where the “D” in D.INSTANCE: means that space for the structure is reserved in the definitions area.
Executing the word SIZE.OF followed by the name of a structure instance leaves the size of the instance on the stack. For example
SIZE.OF BURGER.WORLD .↓ ok 138
which is the size of the CUSTOMER.INFO structure.
Note that structure instances are allowed to cross page boundaries, and executing V.INSTANCE: or D.INSTANCE: may cause the variable pointer VP or the dictionary pointer DP, respectively, to be advanced to a new page.
Heap Instances
It is sometimes useful to instantiate structures in the heap, and this involves a slightly different procedure. To create a named instance of the structure, execute
CUSTOMER.INFO H.INSTANCE: JOE’S.PIZZA↓
Like the previous commands, this creates a definition for JOE’S.PIZZA that, when executed, leaves the base address of the instance on the stack. And SIZE.OF can be used to retrieve the size of the heap instance. However, unlike the previous commands, no memory has yet been assigned. Instead, a parameter field for JOE’S.PIZZA has been created in the variable area, and the size of the structure has been saved with the definition of JOE’S.PIZZA. To allocate memory in heap, we can execute
SIZE.OF JOE’S.PIZZA ‘ JOE’S.PIZZA ALLOCATED↓
The word SIZE.OF removes the next word from the input stream, examines its definition to recover the size (which was put there by H.INSTANCE:) and leaves the size on the stack. ‘ JOE’S.PIZZA leaves the extended parameter field address on the stack, and ALLOCATED converts the size to a double number and calls FROM.HEAP to allocate memory for the structure. Note that we could have replaced the command SIZE.OF JOE’S.PIZZA with CUSTOMER.INFO, as both leave the size of the structure on the stack.
To de-allocate the heap instance, simply execute
‘ JOE’S.PIZZA DEALLOCATED↓
which returns the memory to the heap manager and clears the parameter field. The definition of the structure remains intact for future use.
Nested Structure Definitions
Structures can be nested. For example, the following is a valid structure definition:
STRUCTURE.BEGIN: ADDRESS.LIST
BYTE-> +FLAG
STRUCTURE.BEGIN: SUB
20 STRING-> +NAME
20 STRING-> +ADDRESS
INT-> +ID#
STRUCTURE.END
2 SUB STRUCTS-> +NAME&ADDRESSES
STRUCTURE.END
Embedded within the definition of ADDRESS.LIST is another structure definition for SUB. The member defining word STRUCTS expects on the stack the number of structures and the size of the structures. It names a member with the appropriate offset, and reserves space for the sub-structures in the main structures. Strict hierarchy must be maintained, with each STRUCTURE.END matching its corresponding STRUCTURE.BEGIN: command.
Note that SUB could also have been defined outside of ADDRESS.LIST. The following pair of structures behaves identically to those defined above:
STRUCTURE.BEGIN: SUB
20 STRING-> +NAME
20 STRING-> +ADDRESS
INT-> +ID#
STRUCTURE.END
STRUCTURE.BEGIN: ADDRESS.LIST
BYTE-> +FLAG
3 SUB STRUCTS-> +NAME&ADDRESSES
STRUCTURE.END
If the base xaddress of a particular instance of ADDRESS.LIST is on the stack and we wish to fetch and print the ID# in the first SUB structure, we execute
( base.xaddr -- ) +NAME&ADDRESSES +ID# @ .↓
That is, +NAME&ADDRESSES adjusts the base address to point to the first SUB structure, and +ID# further adjusts the base address to point to the identification number field within the SUB structure.
Multiform Structures
Multiform structures (called “unions” in C and “variant records” in Pascal) are used to define structures whose members may hold different types of data at different times, or whose data may be referred to in several equivalent forms.
For example, the following structure describes the first 6 bytes of the parameter field of a heap object:
STRUCTURE.BEGIN: HEAP.STRUCTURE.PF
TYPE.OF:
XHNDL-> +xHANDLE \ xaddress of the handle to the heap block
OR.TYPE.OF:
PAGE-> +HNDL.PAGE \ handle can be referred to as page...
ADDR-> +HNDL.ADDR \ ... and 16-bit address
OR.TYPE.OF:
PAGE-> +HEAP.PAGE \ page of handle is also page of heap
TYPE.END
ADDR-> +CURRENT.HEAP \ address of the heap used. Its page= handle’s page.
STRUCTURE.END
The TYPE.OF: … OR.TYPE.OF: … TYPE.END syntax is used to define the multiform structure which in this case allows us to refer to a 32-bit quantity in one of three ways, depending on the context of its use.
Structure of an Array or Matrix Parameter Field
The following structure defines the layout of an array or matrix parameter field:
STRUCTURE.BEGIN: MATRIX.PF
HEAP.STRUCTURE.PF
STRUCT-> +HEAP.STRUCTURE.PF \ xHandle and CURRENT.HEAP addr
INT-> +ELEMENT.SIZE \ #Bytes per element
INT-> +#DIMENSIONS \ #dimensions
INT-> +#COLS \ #columns
INT-> +#ROWS \ #rows
INT-> +HANDLER.FLAG \ used by operating system
STRUCTURE.END
Note that the HEAP.STRUCTURE.PF appears as a sub-structure. MATRIX.PF and its synonym, ARRAY.PF are kernel words that leave the size of their respective parameter fields on the stack. They are useful when creating stack frames to hold the parameter fields of temporary matrices and arrays. This is a key technique for writing re-entrant code as explained in the next section.
Designing re-entrant code
For a multitasking system to operate at its full potential, the kernel routines in the system must be “re-entrant”. A re-entrant routine functions properly even if it is “re-entered” while it is executing; this applies to both C and Forth language programs. Routines that are not re-entrant will fail under these circumstances. There are two contexts where this is important: recursion and multitasking.
A recursive routine is one that calls itself; to ensure “re-entrancy with respect to recursion”, a routine may modify only stack-based quantities such as data stack items and local variables (which are kept on the return stack).
Routines that modify only stack or task-private memory locations are “re-entrant with respect to multitasking”. “Task-private” locations are those that can be accessed by one task only. A user variable is a task-private memory location. Routines that modify variables that are shared by more than one task are not re-entrant with respect to multitasking. They may work properly if used by only one task, but are prone to failure if they are called by multiple tasks.
A routine that is re-entrant with respect to recursion is automatically re-entrant with respect to multitasking, but the converse is not true. The rest of this discussion will focus on re-entrancy with respect to multitasking. If you are designing recursive routines, keep in mind that the rules for re-entrancy are stricter than those presented below.
Re-entrant and non-re-entrant definitions
The following definition (originally presented in the prior Chapter) is re-entrant. The word calculates the volume and cross-sectional area of a cylinder, and modifies only the data stack and local variables (which are maintained on the return stack):
: CYLINDER.STATISTICS ( r1\r2 -- r3\r4 )
\ r1 = radius, r2 = height, r3 = volume, r4 = cross-sectional area
LOCALS{ f&height f&radius | f&area }
f&radius FDUP F* PI F* \ cross-sectional area = πR**2
TO f&area
f&area f&height F* ( -- volume = height * area )
f&area ( -- volume\area )
;
The following version of this word is not re-entrant:
REAL: CYLINDER.AREA \ define a self-fetching variable to hold area
: CYLINDER.STATISTICS ( r1\r2 -- r3\r4 )
\ r1 = radius, r2 = height, r3 = volume, r4 = cross-sectional area
LOCALS{ f&height f&radius }
f&radius FDUP F* PI F* \ cross-sectional area = πR**2
TO CYLINDER.AREA
CYLINDER.AREA f&height F* ( -- volume = height * area )
CYLINDER.AREA ( -- volume\area )
;
This second version uses a global self-fetching variable named CYLINDER.AREA. Assume that CYLINDER.STATISTICS is called by task #1, and that the multitasker’s timeslicer transfers control to task#2 just before the final statement of the definition (that is, before the final invocation of CYLINDER.AREA). If task #2 now puts a radius and height on the stack and calls CYLINDER.STATISTICS, the value in the variable CYLINDER.AREA will be changed. When control returns to task#1, the area placed on the stack will be incorrect for that task. This shows how non-re-entrant code causes errors in multitasking systems.
Techniques for ensuring re-entrancy
Each task has its own user area, data and return stacks, POCKET, PAD, and TIB. To ensure re-entrancy, each task that uses the heap must have its own heap area. These memory areas are called “task-private” because only one task has access to them. To ensure re-entrancy, words should modify only stack-based quantities (including local variables) and task-private memory. Non-task-private memory such as the contents of variables and self-fetching variables should not be modified if the word is to be called by more than one task.
To ensure re-entrancy, data structures such as temporary matrices that hold intermediate results within a word must reside wholly on a stack or within a task-private heap. Recall that arrays and matrices have parameter fields that hold dimensioning information and a handle to the heap memory block. The parameter field of a globally defined array or matrix is allotted in the variable area when the data structure is defined. Because a function that uses such a globally defined temporary matrix could be called from multiple tasks, a parameter field in the variable area would make the routine non-re-entrant.
Stack frames
Stack frames solve the problem of how to create a parameter field for a temporary array or matrix while preserving re-entrancy. The temporary array/matrix parameter field can be created and kept on the data stack as a “stack frame”, and using stack-based items preserves re-entrancy.
The kernel word STACK.FRAME allows a structure that has been defined with the structure defining words (see the previous chapter) to be instantiated on the data stack. STACK.FRAME expects a size (in bytes) of the structure to be placed on the data stack, allocates room on the stack, and returns on the top of the stack the extended address of the base of the stack frame. The word FRAME.DROP is used to remove the stack frame from the stack before the calling word finishes executing. The word PF.STACK.FRAME (“parameter-field-stack-frame”) performs the operation of STACK.FRAME and then stores 0\0 into the first 4 bytes of the stack frame in the position where the heap xhandle will reside. Initializing the xhandle to zero is good programming practice that avoids hard-to-diagnose bugs, and PF.STACK.FRAME makes this easy to accomplish when allocating stack-based parameter fields.
Stack frame example
For example, let’s define a re-entrant version of a word that places the transpose of a source matrix into a destination matrix, even if the specified source and destination are the same. If the source and destination matrices are the same we must dimension a temporary matrix, place the transpose into it, and then copy the transpose back to the source. In the discussion of arrays and matrices earlier in this Chapter, we defined the word MAT.TRANSPOSE which can transpose a matrix, but only if the source and the destination are different matrices. We can use MAT.TRANSPOSE to perform the actual transposition, but we’ll add the capability of dimensioning a temporary data matrix to handle the case when the source and the destination are the same.
: SMART.TRANSPOSE ( src.xpfa\dest.xpfa -- )
\ places the transpose of the source in the destination;
\ the destination can be the same as the source
LOCALS{ x&dest x&src | x&temporary }
x&dest x&src X= ( source = destination?--)
IF \ if source = destination...
MATRIX.PF PF.STACK.FRAME \ make room on stack for pf
TO x&temporary \ save temporary xpfa
x&src x&temporary MAT.TRANSPOSE \ transpose is in temp matrix
x&dest x&temporary SWAP.MATRIX \ now x&dest has the answer
x&temporary DELETED \ delete the temporary matrix
MATRIX.PF FRAME.DROP \ clear frame off data stack
ELSE \ else if source not= dest...
x&src x&dest MAT.TRANSPOSE \ ...just do it
ENDIF
;
The first line of the definition loads the source and destination xpfa’s into 32-bit local variables, and creates an un-initialized local variable. The next line checks if the source and destination are equal. If they are not, the ELSE part of the conditional executes, and MAT.TRANSPOSE can perform the operation directly. If the source equals the destination, however, MAT.TRANSPOSE will not work, and we have to create a temporary destination matrix. MATRIX.PF puts the size of the parameter field of the temporary matrix on the stack, and PF.STACK.FRAME allocates space for the parameter field on the data stack and initializes its heap handle to 0\0. The base xaddress of the stack frame is loaded into the local variable x&temporary, and the source and temporary matrix xpfa’s are passed to MAT.TRANSPOSE, which dimensions the temporary matrix and transposes the source into it. Next, SWAP.MATRIX interchanges the contents of the temporary and destination parameter fields so that x&dest is the transposed matrix and x&temporary points to the original matrix. Next the temporary matrix is deleted from the heap, and FRAME.DROP clears the temporary parameter field off the data stack. The use of local variables and stack frames renders the SMART.TRANSPOSE routine re-entrant so it can be used in multitasking applications.
This page is about: Forth Program Development Techniques, Using Interactive Debugging Tools in Forth, Floating Point Mathematics in Forth, Using Forth Heap Manager, Forth Arrays and Matrices, Using Structures in Forth, Designing Re-entrant Code in Forth – Explains features that make it easy to program in QED-Forth, including the interactive debugger, floating point mathematics, heap memory manager, arrays and matrices, structures, and the multitasking executive.