Chapter 18: Floating-Point Numbers

StrongForth's implementation of the ANS Forth Floating-Point word set is provided as source code in blocks 800 to 894. It includes extensions of the StrongForth assembler and disassembler for floating-point instructions, because many words in the Floating-Point word set are implemented as machine code definitions. It is required to load the StrongForth assembler and the disassembler before loading the Floating-Point word set:

100 129 THRU   \ ASSEMBLER
 OK
130 163 THRU   \ DISASSEMBLER
 OK
800 814 THRU   \ FLOATING-POINT ASSEMBLER EXTENSIONS
 OK
815 823 THRU   \ FLOATING-POINT DISASSEMBLER EXTENSIONS
 OK
824 894 THRU   \ FLOATING-POINT WORD SET
 OK

The Floating-Point Stack

Since StrongForth is a 16-bit system, one cell occupies 2 bytes, and a double cell occupies 4 bytes. Floating-point numbers are usually longer. In StrongForth, a floating-point number occupies 8 bytes, because this is the size supported by the hardware floating-point unit of the x86 architecture. Floating-point numbers stored in only 4 bytes do not have the accuracy that is required by most floating-point applications. Internally, the hardware floating-point unit even uses a 10-bytes representation.

Anyway, it's obvious that floating-point numbers can not be a subtype of data types SINGLE or DOUBLE. We need a new ancestor data type for floating-point numbers, whose size is 4 cells:

NULL DATA-TYPE PROCREATES FLOAT 4 CONST,

Of course, subtypes of FLOAT can be created as required.

But there's another fundmental question. The ANS Forth standard allows floating-point numbers to be kept on either the data stack or a separate floating-point stack. Both implementation have advantages and drawbacks. In the so-called unique stack model, floating-point numbers are kept on the data stack, together with other single- and double-cell items. The separate stack model, on the other hand, keeps floating-point numbers on a completely separate stack. It's pretty clear that a separate floating-point stack has a considerable performance advantage on systems that already have a built-in hardware floating-point stack tied to a floating-point unit. Floating-point numbers do not have to be transferred between the data stack and the floating-point unit for each floating-point operation. And there's another big advantage over the unique stack model. ANS Forth specifies stack movement words only for items of the same size. For example, with SWAP, 2SWAP and FSWAP, you can swap two single-cell items, two double-cell items or two floating-point numbers, respectively. But there's no portable way to swap a single-cell or a double-cell item and a floating-point number. Words like SFSWAP ( n r -- r n ) and DFSWAP ( d r -- r d ) are simply not available. In the unique stack model, stack juggling can become very ugly, and words become difficult to maintain. On the other hand, a separate floating-point stack demands from the programmer keeping track of two stacks in parallel, which is not nice either.

So, which of these two models does StrongForth implement? Let's see:

CHAR B 3.1415E0 1000000. .S
CHARACTER FLOAT UNSIGNED-DOUBLE  OK

Okay, that's clearly the unique stack model. StrongForth keeps floating-point numbers on the data stack. Really? Let's try another experiment:

SP@ .
138  OK
CHAR B 3.1415E0 1000000.
 OK
SP@ .
132  OK

Only 6 bytes are on the data stack, although the floating-point number alone has 8 bytes! 6 bytes are just enough to store the character and the double-precision number. Now, that's a clear indication that floating-point numbers are kept on the separate hardware floating-point stack. So what? Here's the solution: The unique stack model is just the programming model. Remember that .S displayes the contents of the data type heap, and not the contents of the data stack. StrongForth combines the advantages of the unique stack model and the separate stack model. You have to keep track of only one stack, but there's no performance drop caused by transfers between the data stack and the floating-point stack.

The floating-point stack has its own stack pointer, that can be fetched and stored similarly to the data stack pointer:

FP@ ( -- UNSIGNED )
FP! ( UNSIGNED -- )
FINIT ( -- )

FINIT initializes the hardware floating-point unit and resets the floating-point stack pointer to 0. The hardware floating-point stack has a depth of 8, and it wraps around from 0 to 7 and from 7 to 0:

FINIT QUIT
FP@ .
0  OK
1E0 FP@ .
7  OK
6.376E-4 FP@ .
6  OK
DROP FP@ .
7  OK
DROP FP@ .
0  OK

Now that the floating-point stack is actually being used, it is necessary that ABORT initializes the hardware floating-point unit as well. In the new version of ABORT, FINIT has been added as the first word to be executed:

:NONAME ( -- )
  FINIT POSTPONE [ SP0 SP! DTP! CR QUIT ; IS ABORT

But what about the second major drawback of the unique stack model? Does StrongForth support stack operations with mixed-size data items? You already know that StrongForth provides stack operations for all combinations of SINGLE and DOUBLE, for example:

SWAP ( DOUBLE DOUBLE -- 2ND 1ST )
SWAP ( DOUBLE SINGLE -- 2ND 1ST )
SWAP ( SINGLE DOUBLE -- 2ND 1ST )
SWAP ( SINGLE SINGLE -- 2ND 1ST )

Of course, StrongForth's Floating-Point word set provides additional overloaded versions for combinations of parameters that include the new ancestor data type FLOAT:

WORDS SWAP
SWAP ( FLOAT FLOAT -- 2ND 1ST )
SWAP ( FLOAT DOUBLE -- 2ND 1ST )
SWAP ( FLOAT SINGLE -- 2ND 1ST )
SWAP ( DOUBLE FLOAT -- 2ND 1ST )
SWAP ( SINGLE FLOAT -- 2ND 1ST )
SWAP ( DOUBLE DOUBLE -- 2ND 1ST )
SWAP ( DOUBLE SINGLE -- 2ND 1ST )
SWAP ( SINGLE DOUBLE -- 2ND 1ST )
SWAP ( SINGLE SINGLE -- 2ND 1ST )
 OK
8.1642E+2  -55 .S
FLOAT SIGNED  OK
SWAP .S
SIGNED FLOAT  OK
. .
816.42 -55  OK

And it works as espected! You can juggle the stacks around using DUP, DROP, SWAP, OVER, ROT, NIP and TUCK with all combinations of SINGLE, DOUBLE and FLOAT data types without considering their size and the stacks these items are located at. If you want to swap a single-cell item and a double-cell item, you naturally use SWAP and not ROT as in ANS Forth. And the same works with floating-point numbers as well. Of course, this is made possible by defining quite a lot of new overloaded versions:

DUP ( FLOAT -- 1ST 1ST )
DUP ( DOUBLE -- 1ST 1ST )
DUP ( SINGLE -- 1ST 1ST )

DROP ( FLOAT -- )
DROP ( DOUBLE -- )
DROP ( SINGLE -- )

OVER ( FLOAT FLOAT -- 1ST 2ND 1ST )
OVER ( FLOAT DOUBLE -- 1ST 2ND 1ST )
OVER ( FLOAT SINGLE -- 1ST 2ND 1ST )
OVER ( DOUBLE FLOAT -- 1ST 2ND 1ST )
OVER ( SINGLE FLOAT -- 1ST 2ND 1ST )
OVER ( DOUBLE DOUBLE -- 1ST 2ND 1ST )
OVER ( DOUBLE SINGLE -- 1ST 2ND 1ST )
OVER ( SINGLE DOUBLE -- 1ST 2ND 1ST )
OVER ( SINGLE SINGLE -- 1ST 2ND 1ST )

ROT ( FLOAT DOUBLE DOUBLE -- 2ND 3RD 1ST )
ROT ( FLOAT DOUBLE SINGLE -- 2ND 3RD 1ST )
ROT ( FLOAT SINGLE DOUBLE -- 2ND 3RD 1ST )
ROT ( FLOAT SINGLE SINGLE -- 2ND 3RD 1ST )
ROT ( FLOAT FLOAT FLOAT -- 2ND 3RD 1ST )
ROT ( FLOAT FLOAT DOUBLE -- 2ND 3RD 1ST )
ROT ( FLOAT FLOAT SINGLE -- 2ND 3RD 1ST )
ROT ( FLOAT DOUBLE FLOAT -- 2ND 3RD 1ST )
ROT ( FLOAT SINGLE FLOAT -- 2ND 3RD 1ST )
ROT ( DOUBLE FLOAT FLOAT -- 2ND 3RD 1ST )
ROT ( DOUBLE FLOAT DOUBLE -- 2ND 3RD 1ST )
ROT ( DOUBLE FLOAT SINGLE -- 2ND 3RD 1ST )
ROT ( DOUBLE DOUBLE FLOAT -- 2ND 3RD 1ST )
ROT ( DOUBLE SINGLE FLOAT -- 2ND 3RD 1ST )
ROT ( SINGLE FLOAT FLOAT -- 2ND 3RD 1ST )
ROT ( SINGLE FLOAT DOUBLE -- 2ND 3RD 1ST )
ROT ( SINGLE FLOAT SINGLE -- 2ND 3RD 1ST )
ROT ( SINGLE DOUBLE FLOAT -- 2ND 3RD 1ST )
ROT ( SINGLE SINGLE FLOAT -- 2ND 3RD 1ST )
ROT ( DOUBLE DOUBLE DOUBLE -- 2ND 3RD 1ST )
ROT ( DOUBLE DOUBLE SINGLE -- 2ND 3RD 1ST )
ROT ( DOUBLE SINGLE DOUBLE -- 2ND 3RD 1ST )
ROT ( DOUBLE SINGLE SINGLE -- 2ND 3RD 1ST )
ROT ( SINGLE DOUBLE DOUBLE -- 2ND 3RD 1ST )
ROT ( SINGLE DOUBLE SINGLE -- 2ND 3RD 1ST )
ROT ( SINGLE SINGLE DOUBLE -- 2ND 3RD 1ST )
ROT ( SINGLE SINGLE SINGLE -- 2ND 3RD 1ST )

NIP ( FLOAT FLOAT -- 2ND )
NIP ( FLOAT DOUBLE -- 2ND )
NIP ( FLOAT SINGLE -- 2ND )
NIP ( DOUBLE FLOAT -- 2ND )
NIP ( SINGLE FLOAT -- 2ND )
NIP ( DOUBLE DOUBLE -- 2ND )
NIP ( DOUBLE SINGLE -- 2ND )
NIP ( SINGLE DOUBLE -- 2ND )
NIP ( SINGLE SINGLE -- 2ND )

TUCK ( FLOAT FLOAT -- 2ND 1ST 2ND )
TUCK ( FLOAT DOUBLE -- 2ND 1ST 2ND )
TUCK ( FLOAT SINGLE -- 2ND 1ST 2ND )
TUCK ( DOUBLE FLOAT -- 2ND 1ST 2ND )
TUCK ( SINGLE FLOAT -- 2ND 1ST 2ND )
TUCK ( DOUBLE DOUBLE -- 2ND 1ST 2ND )
TUCK ( DOUBLE SINGLE -- 2ND 1ST 2ND )
TUCK ( SINGLE DOUBLE -- 2ND 1ST 2ND )
TUCK ( SINGLE SINGLE -- 2ND 1ST 2ND )

27 overloaded versions for ROT! But many of these versions are dummies or aliases, which only occupy memory in the name space. Here are two examples:

' NOOP ALIAS ROT ( FLOAT SINGLE DOUBLE -- 2ND 3RD 1ST )

( DOUBLE SINGLE -- 2ND 1ST )' SWAP
ALIAS ROT ( DOUBLE FLOAT SINGLE -- 2ND 3RD 1ST )

Rotating the floating-point number to the top of the data type heap in the first example does not require any physical data movement, because the floating-point number is located on the floating-point stack and the other two items are (in the correct order) on the data stack. Only the data type heap needs to be adjusted. In the second example, it is sufficient to swap DOUBLE and SINGLE in order to rotate the three items.

Data Type Conversions

You already know the conversion words S>D and D>S from chapter 1. Corresponding conversion words exist for floating-point numbers as well:

S>F ( SINGLE -- FLOAT ) 
S>F ( SIGNED -- FLOAT ) 
D>F ( DOUBLE -- FLOAT ) 
D>F ( SIGNED-DOUBLE -- FLOAT )  
F>S ( FLOAT -- SIGNED )
F>D ( FLOAT -- SIGNED-DOUBLE )

ANS Forth specifies only F>D and the signed version of D>F. In StrongForth, it is also possible to convert unsigned double-precision numbers and signed and unsigned single-precision numbers. Note that the signed versions of S>F and D>F are found first, and the unsigned versions serve as catch-all for all other data types. Let's try some examples:

-815 S>F .
-815.  OK
45611 S>F .
45611.  OK
-6350017. D>F .
-6350017.  OK
3000000000. D>F .
3000000000.  OK
+7.155016E+2 F>S .
716  OK
-7.155016E+5 F>D .
-715502  OK

In order to allow type casts from and to floating-point numbers, the already existing version of CAST needs to be updated. The semantics of the new version is identical to the old version apart from the fact that some additional cases need to be considered:

: CAST ( -- )
  DTDROP DT DUP >DT SWAP DUP >DT SIZE 10 * SWAP SIZE +
  CASE  0 OF              ENDOF
       11 OF              ENDOF
       12 OF POSTPONE S>D ENDOF
       14 OF POSTPONE S>F ENDOF
       21 OF POSTPONE D>S ENDOF
       22 OF              ENDOF
       24 OF POSTPONE D>F ENDOF
       41 OF POSTPONE F>S ENDOF
       42 OF POSTPONE F>D ENDOF
       44 OF              ENDOF
       -271 THROW
  ENDCASE DT> DROP DROP ; IMMEDIATE

NULL needs to be updated to compile the new version of CAST:

: NULL ( -- )
  POSTPONE 0 POSTPONE CAST ; IMMEDIATE

With these two words, all kinds of data type transformations can be accomplished without using the low-level conversion words, which are often unconvenient to use:

3.1415E0 CAST INTEGER .S .
INTEGER 3  OK
-123456789. DUP . CAST FLOAT .
-123456789 -123456789.  OK

Floating-Point Arithmetic

Operator overloading allows StrongForth to get rid of special names for floating-point arithmetic operations. F+ becomes +, F- becomes -, FABS becomes ABS and so on. This is a common standard in most other high-level programming languages. Even better, some of StrongForth's floating-point operations are additionally overloaded with versions for mixed operands. You already know the general rule in StrongForth, that the result of a mixed-mode operation has the same data type as its first operand:

+ ( FLOAT SIGNED -- 1ST )
+ ( FLOAT SIGNED-DOUBLE -- 1ST )
+ ( FLOAT INTEGER -- 1ST )
+ ( FLOAT FLOAT -- 1ST )

- ( FLOAT SIGNED -- 1ST )
- ( FLOAT SIGNED-DOUBLE -- 1ST )
- ( FLOAT INTEGER -- 1ST )
- ( FLOAT FLOAT -- 1ST )

* ( FLOAT SIGNED-DOUBLE -- 1ST )
* ( FLOAT UNSIGNED -- 1ST )
* ( FLOAT SIGNED -- 1ST )
* ( FLOAT FLOAT -- 1ST )

/ ( FLOAT SIGNED-DOUBLE -- 1ST )
/ ( FLOAT UNSIGNED -- 1ST )
/ ( FLOAT SIGNED -- 1ST )
/ ( FLOAT FLOAT -- 1ST )

ABS ( FLOAT -- 1ST )
NEGATE ( FLOAT -- 1ST )
MIN ( FLOAT 1ST -- 1ST )
MAX ( FLOAT 1ST -- 1ST )
ROUND ( FLOAT -- 1ST )
FLOOR ( FLOAT -- 1ST )

Basic floating-point arithmetics with mixed data types are available for data types SIGNED, UNSIGNED and SIGNED-DOUBLE, but not for data type UNSIGNED-DOUBLE. This is because data type UNSIGNED-DOUBLE is not directly supported by the hardware floating-point unit. But you can easily define the missing words yourself, for example like this:

: + ( FLOAT UNSIGNED-DOUBLE -- 1ST )
  D>F + ;

Apart from this restriction, you can use floating-point numbers in the same way as you use single-precision or double-precision integer numbers:

6.279E+6 +100000.E0 +200000.E0 .S
FLOAT FLOAT FLOAT  OK
MAX DUP .
200000.  OK
+ .
6479000.  OK
6.279E+6 +100000. +200000. .S
FLOAT SIGNED-DOUBLE SIGNED-DOUBLE  OK
MAX DUP .
200000  OK
+ .
6479000.  OK
+1000000. 2 7.71063E-4 .S
SIGNED-DOUBLE UNSIGNED FLOAT  OK
ROT * DUP . OVER .S
771.063 UNSIGNED FLOAT UNSIGNED  OK
/ . .
385.5315 2  OK

The comparison operators do not need the "F" prefix in their name, because they are overloaded as well:

= ( FLOAT 1ST -- FLAG )
<> ( FLOAT 1ST -- FLAG )
< ( FLOAT 1ST -- FLAG )
> ( FLOAT 1ST -- FLAG )
0= ( FLOAT -- FLAG )
0<> ( FLOAT -- FLAG )
0< ( FLOAT -- FLAG )
0> ( FLOAT -- FLAG )

A good illustration of how to use the floating-point comparison operators is the definition of ~ (F~ in ANS Forth).

: ~ ( FLOAT FLOAT FLOAT -- FLAG )
  DUP 0= IF DROP = EXIT THEN DUP 0>
  IF ROT ROT - ABS SWAP <
  ELSE ROT ROT OVER OVER - ABS
     ROT ABS ROT ABS + ROT ABS * <
  THEN ;

And finally, StrongForth provides the floating-point constants 0 and 1. Since these two constants are used quite frequently, the hardware floating-point unit has dedicated machine instructions for loading them:

0E0 ( -- FLOAT )
1E0 ( -- FLOAT )

Advanced Floating-Point Operations

ANS Forth specifies the trancendental floating-point operations as extension words. Those operations that can be directly calculated by the hardware floating-point unit are implemented as machine-code definitions:

LN ( FLOAT -- 1ST )
LNP1 ( FLOAT -- 1ST )
LOG ( FLOAT -- 1ST )
EXPM1 ( FLOAT -- 1ST )
ALOG ( FLOAT -- 1ST )
** ( FLOAT FLOAT -- 1ST )
SIN ( FLOAT -- 1ST )
COS ( FLOAT -- 1ST )
SINCOS ( FLOAT -- 1ST 1ST )
ATAN2 ( FLOAT FLOAT -- 1ST )
SQRT ( FLOAT -- 1ST )
PI ( -- FLOAT )

PI is not specified by ANS Forth, but it is included in the StrongForth Floating-Point word set anyway, because it is a predefined constant of the hardware floating-point unit. The other ANS Forth floating-point operations are colon definitions:

: EXP ( FLOAT -- 1ST )
  EXPM1 1E0 + ;

: TAN ( FLOAT -- 1ST )
  SINCOS / ;

: SINH ( FLOAT -- 1ST )
  EXPM1 DUP DUP 1E0 + / + 2E0 / ;

: COSH ( FLOAT -- 1ST )
  EXPM1 DUP DUP 1E0 + / - 2E0 / 1E0 + ;

: TANH ( FLOAT -- 1ST )
  EXPM1 DUP DUP 1E0 + / OVER OVER + ROT ROT - 2E0 + / ;
  
: ATAN ( FLOAT -- 1ST )
  1E0 ATAN2 ;

: ASIN ( FLOAT -- 1ST )
  DUP DUP * NEGATE 1E0 + SQRT ATAN2 ;

: ACOS ( FLOAT -- 1ST )
  DUP DUP * NEGATE 1E0 + SQRT SWAP ATAN2 ;
  
: ATANH ( FLOAT -- 1ST )
  DUP LNP1 SWAP NEGATE LNP1 - 0.5E0 * ;

: ASINH ( FLOAT -- 1ST )
  DUP DUP * 1E0 + SQRT / ATANH ;

: ACOSH ( FLOAT -- 1ST )
  DUP DUP * 1E0 - SQRT SWAP / ATANH ;

Note that none of these words has an "F" as a prefix in its name. StrongForth generally does not have such prefixes, because the interpreter and the compiler are able to distinguish words that are overloaded for different data types.

Floating-Point Numbers In Memory

The representation of a floating-point number in memory occupies 8 bytes by default. StrongForth simply overloads the words @, ! and +! to access floating-point numbers in memory:

@ ( FAR-ADDRESS -> FLOAT -- 2ND )
@ ( DATA -> FLOAT -- 2ND )
@ ( CONST -> FLOAT -- 2ND )
@ ( CODE -> FLOAT -- 2ND )

! ( FLOAT FAR-ADDRESS -> 1ST -- )
! ( FLOAT DATA -> 1ST -- )
! ( FLOAT CONST -> 1ST -- )
! ( FLOAT CODE -> 1ST -- )

+! ( SIGNED DATA -> FLOAT -- )
+! ( SIGNED-DOUBLE DATA -> FLOAT -- )
+! ( INTEGER DATA -> FLOAT -- )
+! ( FLOAT DATA -> FLOAT -- )

Furthermore, StrongForth provides overloaded versions of the following words, that already exist for both single-cell and double-cell items:

, ( FLOAT -- )
CONST, ( FLOAT -- )
FILL ( DATA -> FLOAT UNSIGNED 2ND -- )
ERASE ( DATA -> FLOAT UNSIGNED -- )
MOVE ( CONST -> FLOAT DATA -> 2ND UNSIGNED -- )
MOVE ( DATA -> FLOAT DATA -> 2ND UNSIGNED -- )

Generally, StrongForth overloads words that can be applied to single-cell items for items with a different size (DOUBLE and FLOAT) in all cases where it makes sense. Since it's not necessary to attach specific prefixes like "2", "D" or "F" to these versions, there's no need to memorize the additional versions. They are simply available whenever they are needed. Their existence is a matter of consistency.

Memory addresses of floating-point numbers are denoted by ADDRESS -> FLOAT, FAR-ADDRESS -> FLOAT or subtypes like DATA -> FLOAT. Any arithmetic with these addresses needs to consider the size of a floating-point number in memory, just like address arithmetic with addresses of single-cell and double-cell numbers. Of course, StrongForth provides overloaded words for this purpose:

+ ( FAR-ADDRESS -> FLOAT INTEGER -- 1ST )
+ ( ADDRESS -> FLOAT INTEGER -- 1ST )
- ( ADDRESS -> FLOAT 1ST -- SIGNED )
- ( FAR-ADDRESS -> FLOAT INTEGER -- 1ST )
- ( ADDRESS -> FLOAT INTEGER -- 1ST )
1+ ( FAR-ADDRESS -> FLOAT -- 1ST )
1+ ( ADDRESS -> FLOAT -- 1ST )
1- ( FAR-ADDRESS -> FLOAT -- 1ST )
1- ( ADDRESS -> FLOAT -- 1ST )
+! ( INTEGER DATA -> FAR-ADDRESS -> FLOAT -- )
+! ( INTEGER DATA -> ADDRESS -> FLOAT -- )
(LOOP) ( ADDRESS -> FLOAT -- )
(+LOOP) ( INTEGER ADDRESS -> FLOAT -- )

FLOATS is another word that supports address arithmetic:

: FLOATS ( INTEGER -- 1ST )
  8 * ;

In StrongForth, FLOATS is only needed for low-level address arithmetic with unspecified addresses, because address arithmetic with explicit floating-point addresses automatically considers the size of floating-point numbers in memory:

HERE .S .
ADDRESS 2720  OK
HERE 2 FLOATS + .
2736  OK
HERE -> FLOAT 2 + .
2736  OK

For compatibility with ANS Forth, FALIGN and FALIGNED are provided as well. Since the x86 architecture has no restrictions on the alignment of floating-point numbers, FALIGN and FALIGNED simply map to normal cell alignment:

' ALIGN   ALIAS FALIGN   ( -- )
' ALIGNED ALIAS FALIGNED ( ADDRESS -- 1ST )
' ALIGNED ALIAS FALIGNED ( FAR-ADDRESS -- 1ST )

Finally, how can you create the address of a floating-point number without using a clumsy type cast like HERE -> FLOAT? The easiest way is to create a floating-point variable with the overloaded version of VARIABLE:

WORDS VARIABLE
VARIABLE ( FLOAT -- )
VARIABLE ( DOUBLE -- )
VARIABLE ( SINGLE -- )
 OK
6.3E0 VARIABLE VOLTAGE
 OK
VOLTAGE .S @ .
DATA -> FLOAT 6.3  OK
12.7E0 VOLTAGE ! VOLTAGE @ .
12.7  OK
3 VOLTAGE +! VOLTAGE @ .
15.7  OK

Again, thanks to operator overloading, no new name needs to be introduced for the floating-point version of VARIABLE. VARIABLE expects an initialization value on the data space, which allows distinguishing the different overloaded versions and determining the precise data type of the variable. Of course, floating-point versions of CONSTANT and VALUE are available as well:

WORDS CONSTANT
CONSTANT ( FLOAT -- )
CONSTANT ( DOUBLE -- )
CONSTANT ( SINGLE -- )
 OK
1.3804E-23 CONSTANT BOLTZMANN
 OK
BOLTZMANN .S .
FLOAT 0.000000000000000000000013804  OK
WORDS VALUE
VALUE ( FLOAT -- )
VALUE ( DOUBLE -- )
VALUE ( SINGLE -- )
 OK
 36.8E0 VALUE TEMPERATURE
 OK
TEMPERATURE . ." °C"
36.8 °C OK
38.9E0 TO TEMPERATURE
 OK
TEMPERATURE . ." °C"
38.9 °C OK

Note that ANS Forth specifies FVARIABLE and FCONSTANT, but no floating-point version of VALUE.

The only thing that needs to be changed in TO and +TO is the test for the parsed name being the name of a value. Since this test is factored into the deferred definition ?VALUE, it is sufficient to extend the semantics of ?VALUE accordingly:

MARKER FENCE 0E0 VALUE FNULL 'CODE FNULL FENCE

:NONAME ( CDATA -> CHARACTER UNSIGNED -- DEFINITION )
  LOCALS| COUNT ADDR |
  ADDR COUNT ['CODE] SOURCE-ID CODE-FIELD SEARCH-ALL
  IF EXIT THEN DROP
  ADDR COUNT ['CODE] LATEST CODE-FIELD SEARCH-ALL
  IF EXIT THEN DROP
  ADDR COUNT [ ROT ] LITERAL CODE-FIELD SEARCH-ALL
  IF EXIT THEN -32 THROW ; IS ?VALUE

The phrase enclosed by MARKER FENCE ... FENCE just determines the code field of a floating-point value. This is necessary because there's no predefined floating-point value that can serve this purpose. The code field is compiled into the definition of ?VALUE with [ ROT ] LITERAL.

IEEE Floating-Point Representation

In addition to the implementation dependent representation of floating-point numbers in memory, ANS Forth specifies predefined 64-bit and 32-bit representations according to IEEE as extension words. It is important to note that these representations need to be considered only in memory. The internal representations of floating-point numbers on the hardware floating-point stack is always the same. So, instead of defining new data types for floating-point numbers, it is sufficient to define new data types for addresses of floating-point numbers along with corresponding subtypes for the various memory spaces:

DT ADDRESS PROCREATES SFADDRESS
DT SFADDRESS PROCREATES SFDATA
DT SFADDRESS PROCREATES SFCONST
DT SFADDRESS PROCREATES SFCODE
DT FAR-ADDRESS PROCREATES SFFAR-ADDRESS

DT ADDRESS PROCREATES DFADDRESS
DT DFADDRESS PROCREATES DFDATA
DT DFADDRESS PROCREATES DFCONST
DT DFADDRESS PROCREATES DFCODE
DT FAR-ADDRESS PROCREATES DFFAR-ADDRESS

An address of a memory location that contains a floating-point number in 32-bit IEEE representation is of data type SFADDRESS or one of its subtypes, whereas an address of a floating-point number in 64-bit IEEE representation has data type DFADDRESS or one of its subtypes. For these additional representations, overloaded versions of the fetch and store instructions, as well as address arithmetic instructions are available:

@ ( SFFAR-ADDRESS -> FLOAT -- 2ND )
@ ( SFCODE -> FLOAT -- 2ND )
@ ( SFCONST -> FLOAT -- 2ND )
@ ( SFDATA -> FLOAT -- 2ND )
! ( FLOAT SFFAR-ADDRESS -> 1ST -- )
! ( FLOAT SFCODE -> 1ST -- )
! ( FLOAT SFCONST -> 1ST -- )
! ( FLOAT SFDATA -> 1ST -- )
+! ( SIGNED SFDATA -- )
+! ( SIGNED-DOUBLE SFDATA -- )
+! ( INTEGER SFDATA -- )
+! ( FLOAT SFDATA -- )
FILL ( SFDATA -> FLOAT UNSIGNED 2ND -- )
ERASE ( SFDATA UNSIGNED -- )
MOVE ( SFCONST -> FLOAT SFDATA -> 2ND UNSIGNED -- )
MOVE ( SFDATA -> FLOAT SFDATA -> 2ND UNSIGNED -- )
+ ( SFFAR-ADDRESS INTEGER -- 1ST )
+ ( SFADDRESS INTEGER -- 1ST )
- ( SFADDRESS 1ST -- SIGNED )
- ( SFFAR-ADDRESS INTEGER -- 1ST )
- ( SFADDRESS INTEGER -- 1ST )
1+ ( SFFAR-ADDRESS -- 1ST )
1+ ( SFADDRESS -- 1ST )
1- ( SFFAR-ADDRESS -- 1ST )
1- ( SFADDRESS -- 1ST )
+! ( INTEGER DATA -> SFFAR-ADDRESS -- )
+! ( INTEGER DATA -> SFADDRESS -- )
(LOOP) ( SFADDRESS -- )
(+LOOP) ( INTEGER SFADDRESS -- )

@ ( DFFAR-ADDRESS -> FLOAT -- 2ND )
@ ( DFCODE -> FLOAT -- 2ND )
@ ( DFCONST -> FLOAT -- 2ND )
@ ( DFDATA -> FLOAT -- 2ND )
! ( FLOAT DFFAR-ADDRESS -> 1ST -- )
! ( FLOAT DFCODE -> 1ST -- )
! ( FLOAT DFCONST -> 1ST -- )
! ( FLOAT DFDATA -> 1ST -- )
+! ( SIGNED DFDATA -- )
+! ( SIGNED-DOUBLE DFDATA -- )
+! ( INTEGER DFDATA -- )
+! ( FLOAT DFDATA -- )
FILL ( DFDATA -> FLOAT UNSIGNED 2ND -- )
ERASE ( DFDATA UNSIGNED -- )
MOVE ( DFCONST -> FLOAT DFDATA -> 2ND UNSIGNED -- )
MOVE ( DFDATA -> FLOAT DFDATA -> 2ND UNSIGNED -- )
+ ( DFFAR-ADDRESS INTEGER -- 1ST )
+ ( DFADDRESS INTEGER -- 1ST )
- ( DFADDRESS 1ST -- SIGNED )
- ( DFFAR-ADDRESS INTEGER -- 1ST )
- ( DFADDRESS INTEGER -- 1ST )
1+ ( DFFAR-ADDRESS -- 1ST )
1+ ( DFADDRESS -- 1ST )
1- ( DFFAR-ADDRESS -- 1ST )
1- ( DFADDRESS -- 1ST )
+! ( INTEGER DATA -> DFFAR-ADDRESS -- )
+! ( INTEGER DATA -> DFADDRESS -- )
(LOOP) ( DFADDRESS -- )
(+LOOP) ( INTEGER DFADDRESS -- )

Note that except for @, !, FILL and MOVE, addresses of IEEE floating-point numbers do not need to be compound data types (like ADDRESS -> FLOAT), because the basic data types SFADDRESS and DFADDRESS implicitly point to floating-point numbers. In addition to these overloaded words, the following ANS Forth words have been added:

SFLOATS ( INTEGER -- 1ST )
SFALIGN ( -- )
SFALIGNED ( ADDRESS -- 1ST )
SFALIGNED ( FAR-ADDRESS -- 1ST )

DFLOATS ( INTEGER -- 1ST )
DFALIGN ( -- )
DFALIGNED ( ADDRESS -- 1ST )
DFALIGNED ( FAR-ADDRESS -- 1ST )

Again, the alignment words are simple aliases of ALIGN and ALIGNED, because the x86 architecture does not demand strict alignment for floating-point numbers.

Floating-Point Literals

StrongForth's interpreter needs to be extended in order to interpret and compile floating-point numbers. Since INTERPRET is deferred, its definition can easily be replaced:

:NONAME ( -- )
  BEGIN PARSE-WORD DUP
  WHILE OVER OVER SEARCH-LOCAL DUP
     IF ROT DROP ROT DROP ABS LOCAL,
     ELSE ELSE DROP DROP OVER OVER FALSE MATCH SEARCH-ALL DUP
        IF ROT DROP ROT DROP 0< STATE @ AND
           IF COMPILE, 
           ELSE FALSE DT>DT (EXECUTE) 
           THEN
        ELSE DROP DROP OVER OVER NUMBER DUP 0=
           IF DROP DROP >FLOAT'
              IF [DT] FLOAT >DT STATE @
                 IF LITERAL, 
                 ELSE ( FLOAT -- )CAST 
                 THEN
              ELSE DROP -13 THROW
              THEN
           ELSE ROT DROP ROT DROP DUP >DT SIZE 1- STATE @
              IF 
                 IF LITERAL, 
                 ELSE D>S LITERAL, 
                 THEN
              ELSE
                 IF ( DOUBLE -- )CAST 
                 ELSE D>S ( SINGLE -- )CAST
                 THEN
              THEN
           THEN
        THEN
     THEN
  REPEAT DROP DROP ; IS INTERPRET

Words that have been added to the non-floating-point version of INTERPRET are underlined. Whereas the old version of INTERPRET throws an exception if NUMBER fails to convert the parsed word into a number, gives the updated version the interpreter another chance by trying to convert the word into a floating-point number. The conversion is done by >FLOAT'. Depending on STATE, the floating-point number is either compiled as a literal, or kept on the stack. For compiling a floating-point number, StrongForth provides an overloaded version of LITERAL,:

: LITERAL, ( FLOAT -- )
  ['TOKEN] FLIT CONST, F, ;

Remember that LITERAL, is already overloaded for items of data types SINGLE and DOUBLE. FLIT is the token of a floating-point literal. Since floating-point literals are stored in the internal 80-bit represetentation of the hardware floating-point unit, LITERAL, uses F, instead of CONST,. F, compiles a floating-point number in a 5 cells representation into to the constant data space. Only two other words, F@ and F!, give access to floating-point numbers in this format, and only the constant data space is a possible location:

FLIT ( -- FLOAT )
F, ( FLOAT -- )
F@ ( CONST -> FLOAT -- 2ND )
F! ( FLOAT CONST -> 1ST -- )

In order to complete the number of overloaded versions for dealing with floating-point literals, StrongForth provides a floating-point version of LITERAL. In ANS Forth, this word is called FLITERAL:

: LITERAL ( FLOAT -- )
  ?COMPILE POSTPONE [ DTP@ ] @>DT LITERAL, ; IMMEDIATE

Floating-point numbers can also be stored on the return stack. The words >R, R@ and R> work exactly as expected. But that doesn't mean that all of these words have to be overloaded. For >R to work with floating-point numbers as well, all that is needed is an overloaded version of (>R). R@ is dynamically defined as a local:

: >R ( -- R-SIZE )
  ?COMPILE POSTPONE (>R)
  DTP@ @ SIZE DUP #LOCALS +!
  " R@" TRANSIENT CREATE-LOCAL CAST R-SIZE ; IMMEDIATE

However, for R> a new version is required. The only difference to the version that does not consider floating-point numbers is an additional case within the CASE ... ENDCASE structure that contains a new word (FRDROP):

: R> ( R-SIZE -- )
  ?COMPILE POSTPONE R@ CAST SIGNED DUP FORGET-LOCAL
  CASE +1 OF POSTPONE (RDROP)  ENDOF
       +2 OF POSTPONE (DRDROP) ENDOF
       +4 OF POSTPONE (FRDROP) ENDOF
  -271 THROW ENDCASE ; IMMEDIATE

In total three new low-level words are required to handle floating-point numbers on the return stack:

(>R) ( FLOAT -- )
(FRDROP) ( -- )
(FR@) ( -- FLOAT )

(FR@) is used for dealing with values and locals. StrongForth supports floating-point values and locals, although they are not specified by ANS Forth.

LOCAL,, which is used by INTERPRET for compiling references to locals, needs just a minor addition with respect to the non-floating-point version:

: LOCAL, ( DATA -> DATA-TYPE SIGNED -- )
  ?COMPILE #LOCALS @ SWAP - DUP 0<
  IF DROP DROP -263 THROW
  ELSE OVER @ SIZE
     CASE 1 OF ['TOKEN] (R@)  ENDOF
          2 OF ['TOKEN] (DR@) ENDOF
          4 OF ['TOKEN] (FR@) ENDOF
     -271 THROW ['TOKEN] NOOP SWAP ENDCASE
     CONST, CELLS CONST, @>DT
  THEN ;

An important point to mention is the fact that StrongForth's implementation of locals not only allows defining locals for all data types. It even allows to arbitrarily mix data types of different sizes within one LOCALS| ... | structure. It is not necessary to separate locals for single-cell items, double-cell items and floating-point numbers in different LOCALS| ... | structures. In fact, multiple LOCALS| ... | structure within the same definition are not allowed. Here's an example of valid StrongForth code:

: FOO ( SIGNED-DOUBLE FLOAT -- CHARACTER )
  [CHAR] % LOCALS| C F SD |
  \ ... \
  F SD 8 * + . C .
  \ ... \
  C ;

TO and +TO don't need to be extended for dealing with floating-point locals, because they automatically compile the correct versions of ! and +!, respectively.

Input

For converting a character string representation of a floating-point number into the internal binary representation, ANS Forth specifies the word >FLOAT. The conversion complies with a quite universal rule, which even allows character strings like .4d8 or 6-3 to be converted into valid floating-point numbers:

Convertible string := <significand>[<exponent>] 
<significand>      := [<sign>]{<digits>[.<digits0>] | .<digits> }
<exponent>         := <marker><digits0>
<marker>           := {<e-form> | <sign>}
<e-form>           := <e-char>[<sign>]
<sign>             := { + | - }
<e-char>           := { D | d | E | e }
<digits>           := <digit><digits0>
<digits0>          := <digit>*
<digit>            := { 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }

This rule gives some guidance for a reasonable factoring of >FLOAT. At the lower end, >CHAR checks whether the first character of a string is a given character. If it is, the character is removed from the string. An empty string or a string that does not start with this character are left unchanged. An obvious application of >CHAR is >PERIOD:

: >CHAR ( CDATA -> CHARACTER UNSIGNED 2ND -- 1ST 3RD FLAG )
  LOCALS| C | DUP
  IF OVER @ C = IF /STRING TRUE ELSE FALSE THEN
  ELSE FALSE
  THEN ;
  
: >PERIOD ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD FLAG )
  [CHAR] . >CHAR ;

>SIGN, >E-CHAR and MARKER are implementations of the sub-rules <sign>, <e-char> and <marker> in the above conversion rule, respectively. Note that >SIGN ( CHARACTER -- SIGNED ) is a word that belongs to StrongForth's Core word set. >SIGN and >MARKER return the converted sign as -1 or +1, or 0 if the conversion did not succeed.

: >SIGN ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD SIGNED )
  DUP
  IF OVER @ >SIGN DUP IF >R /STRING R> THEN
  ELSE +0
  THEN ;

: >E-CHAR ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD FLAG )
  DUP
  IF OVER @ [ 0 BIT 5 BIT OR INVERT ] LITERAL AND [CHAR] D =
     DUP IF >R /STRING R> THEN
  ELSE FALSE
  THEN ;

: >MARKER ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD SIGNED )
  >E-CHAR >R >SIGN R> OVER
  IF DROP ELSE IF 1+ THEN THEN ;

>EXP and >DIGITS both implement <digits0>, for the exponent and for the significant, respectively. >EXP expects the sign converted by >MARKER and returns the converted exponent as a signed single-precision number. >DIGITS accumulates decimal digits into the floating-point number FLOAT and returns the number of digits that were converted:

: >EXP ( CDATA -> CHARACTER UNSIGNED SIGNED -- 1ST 3RD 4 TH FLAG )
  DUP IF >R 0. ROT ROT >NUMBER ROT CAST SIGNED R> * TRUE
  ELSE FALSE
  THEN ;

: >DIGITS ( FLOAT CDATA -> CHARACTER UNSIGNED -- 1ST 2ND 4 TH SIGNED )
  +0 LOCALS| N |
  BEGIN DUP
  WHILE OVER @ DIGIT?
  WHILE 1 +TO N >R ROT 10 * R> + ROT ROT /STRING
  REPEAT DROP
  THEN N ;

The conversion rule for the significand allows two different formats:

>SIGNIFICAND' converts a character string according to the first rule, starting after the sign. It returns FALSE as FLAG if the string does not begin with a decimal digit. >SIGNIFICAND converts according to either of both rules. If the character string starts with a decimal point, it converts the string according to the second rule. Otherwise, it tries the first rule by executing >SIGNIFICAND'.

: >SIGNIFICAND' ( FLOAT CDATA -> CHARACTER UNSIGNED -- 1ST 2ND 4 TH FLAG )
  >DIGITS
  IF >PERIOD IF >DIGITS >R ROT R> /10^N ROT ROT THEN TRUE
  ELSE FALSE
  THEN ;

: >SIGNIFICAND ( FLOAT CDATA -> CHARACTER UNSIGNED -- 1ST 2ND 4 TH FLAG )
  >PERIOD
  IF >DIGITS DUP
     IF >R ROT R> /10^N ROT ROT TRUE ELSE DROP FALSE THEN
  ELSE >SIGNIFICAND'
  THEN ;

Now it's possible to implement >FLOAT. Two simple words, NOT-DECIMAL? and ?NEGATE have additionally been factored out. NOT-DECIMAL? returns TRUE if the number-conversion radix is different from 10 (decimal). In this case, >FLOAT aborts the conversion. ?NEGATE is used to apply the sign of the significand to the floating-point number. >FLOAT has several exit points that are taken if the format does not comply with the conversion rule. As a special case, >FLOAT converts an empty character string into a valid floating-point number with the value zero:

: NOT-DECIMAL? ( -- FLAG )
  BASE @ 10 <> ;

: ?NEGATE ( FLOAT SIGNED -- 1ST )
  0< IF NEGATE THEN ;

: >FLOAT ( CDATA -> CHARACTER UNSIGNED -- FLOAT FLAG )
  0E0 ROT ROT DUP 0= IF DROP DROP TRUE EXIT THEN
  >SIGN >R DUP 0= NOT-DECIMAL? OR IF DROP DROP FALSE EXIT THEN
  >SIGNIFICAND INVERT IF DROP DROP FALSE EXIT THEN
  DUP
  IF >MARKER >EXP INVERT IF DROP DROP DROP FALSE EXIT THEN
     ROT DROP SWAP IF DROP FALSE EXIT THEN *10^N
  ELSE DROP DROP
  THEN R> ?NEGATE TRUE ;

Unfortunately, >FLOAT cannot be used by the interpreter to convert floating-point numbers within parsed StrongForth code. The conversion rule according to the ANS Forth specification is more strict in this case:

Convertible string := <significand><exponent> 
<significand>      := [<sign>]<digits>[.<digits0>]
<exponent>         := E[<sign>]<digits0>
<sign>             := { + | - }
<digits>           := <digit><digits0>
<digits0>          := <digit>*
<digit>            := { 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }

The conversion rule for the significant is identical to the one that is already implemented in >SIGNIFICANT'. For the exponent, a variant of >MARKER is needed, which requires a capital "E". This variant is called >MARKER'. With >SIGNIFICAND' and >MARKER', >FLOAT' can be definied. >FLOAT' implements the strict floating-point conversion rule that is required for the interpreter:

: >MARKER' ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD SIGNED )
  [CHAR] E >CHAR
  IF >SIGN DUP 0= IF 1+ THEN ELSE +0 THEN ;

: >FLOAT' ( CDATA -> CHARACTER UNSIGNED -- FLOAT FLAG )
  0E0 ROT ROT >SIGN LOCALS| S |
  DUP 0= NOT-DECIMAL? OR IF DROP DROP FALSE EXIT THEN
  >SIGNIFICAND' INVERT IF DROP DROP FALSE EXIT THEN
  DUP
  IF >MARKER' >EXP INVERT IF DROP DROP DROP FALSE EXIT THEN
     ROT DROP SWAP IF DROP FALSE EXIT THEN *10^N
     S ?NEGATE TRUE
  ELSE DROP DROP FALSE
  THEN ;

Output

Pictured numeric output is based on the ANS Forth word REPRESENT. REPRESENT creates a representation of the absolute value of the significand of a floating-point number and stores it into a buffer. Furthermore, it returns the sign of the significand, the exponent, and a flag telling whether the floating-point number is a valid number.

: REPRESENT ( FLOAT CDATA -> CHARACTER UNSIGNED -- SIGNED FLAG FLAG )
  ?DECIMAL OVER OVER [CHAR] 0 FILL
  [ HEX ] ROT FXAM 0200 OVER AND 0<> TRUE +0 LOCALS| E F S |
  4500 SWAP AND
  CASE 0400 OF REP-VALID TO E ENDOF
       4000 OF DROP DROP DROP ENDOF
  NIP REP-INVALID FALSE TO F NULL LOGICAL ENDCASE
  [ DECIMAL ] E S F ;

?DECIMAL throws an exception if the current number-conversion radix is not 10 (decimal)

: ?DECIMAL ( -- )
  NOT-DECIMAL? IF -40 THROW THEN ;

FXAM is a word that returns the content of the hardware floating-point unit's status word register after examining the number on top of the floating-point stack:

FXAM ( -- LOGICAL )

Bits 14, 10, 9 and 8 are set depending on the status of this number:

Status Bit 14 Bit 10 Bit 9 Bit 8
Unsupported 0 0 s 0
NaN (Not a Number) 0 0 s 1
Valid 0 1 s 0
Infinity 0 1 s 1
Zero 1 0 s 0
Free 1 x x 1
Denormalized 1 1 s 0

x is undefined. s is 0 if the number is positive, and 1 if it is negative. Valid numbers are converted to digits by REP-VALID, while invalid numbers are handled by REP-INVALID. Let's first have a look at invalid numbers:

: REP-INVALID ( CDATA -> CHARACTER UNSIGNED UNSIGNED -- )
  ROT ROT LOCALS| COUNT ADDR |
  CASE [ HEX ] 0000 OF " UNSUPPORTED"  ENDOF
               0100 OF " NAN"          ENDOF
               0500 OF " INFINITY"     ENDOF
               4400 OF " DENORMALIZED" ENDOF
  [ DECIMAL ] " FREE" ROT ENDCASE
  ADDR COUNT BLANK ADDR COUNT ROT MIN MOVE ;

REP-INVALID fills the buffer with a character string indicating the status of the number according to the information delivered by FXAM. If the buffer is longer than the character string, trailing blanks are added. If the buffer is too short to take the whole character string, the character string is simply cut off.

Now, here's how REP-VALID works. REP-VALID calculates the decimal exponent of the floating-point number and then scales the number in such a way that its integer part contains exactly as many digits as fit into the buffer. (REPRESENT) is a machine code word that uses the hardware floating-point unit's BCD arithmetic to convert the upto 18 digits long integer part of the floating-point number right-aligned to a character string:

: REP-VALID ( CDATA -> CHARACTER UNSIGNED FLOAT -- SIGNED )
  ABS SWAP 18 MIN TUCK CAST SIGNED OVER LOG F>S DUP LOCALS| E |
  - *10^N OVER 1E0 SWAP CAST SIGNED *10^N OVER ROUND > INVERT
  IF +10 S>F / 1 +TO E
  THEN (REPRESENT) E ;

(REPRESENT) ( CDATA -> CHARACTER UNSIGNED FLOAT -- )

For scaling, REP-VALID uses *10^N, which in turn uses /10^N. These two words multiply/divide a floating-point number by/through a given power of 10, using multiplications and divisions in order to keep the accuracy:

: /10^N ( FLOAT SIGNED -- 1ST )
  ABS LOCALS| N | +10 S>F
  BEGIN N 0<>
  WHILE N +2 /MOD TO N IF TUCK / SWAP THEN DUP *
  REPEAT DROP ;

: *10^N ( FLOAT SIGNED -- 1ST )
  DUP 0< IF /10^N EXIT THEN
  LOCALS| N | +10 S>F
  BEGIN N 0<>
  WHILE N +2 /MOD TO N IF TUCK * SWAP THEN DUP *
  REPEAT DROP ;

The overloaded version of . for floating-point numbers uses REPRESENT and a number of pretty simple new words. The semantics of . is the same as the one of F. in ANS Forth.

: PERIOD ( -- )
  [CHAR] . . ;

: ZERO ( -- )
  [CHAR] 0 . ;

: ZEROS ( INTEGER -- )
  CAST SIGNED +0 MAX +0 ?DO ZERO LOOP ;

: -TRAILINGZEROS ( CDATA -> CHARACTER UNSIGNED -- 1ST 3RD )
  BEGIN DUP
  WHILE OVER OVER + 1- @ [CHAR] 0 =
  WHILE 1-
  REPEAT THEN ;
  
: .SIGN ( FLAG -- )
  IF [CHAR] - . THEN ;

16 VALUE PRECISION

: SET-PRECISION ( UNSIGNED -- )
  18 MIN TO PRECISION ;

: . ( FLOAT -- )
  TRANS-BOTTOM PRECISION REPRESENT ROT LOCALS| M |
  IF .SIGN TRANS-BOTTOM PRECISION M 0>
     IF OVER OVER M CAST UNSIGNED MIN TYPE
        M PRECISION - ZEROS
        PERIOD PRECISION M CAST UNSIGNED MIN /STRING
     ELSE ZERO PERIOD M NEGATE ZEROS
     THEN -TRAILINGZEROS TYPE
  ELSE DROP TRANS-BOTTOM PRECISION -TRAILING TYPE
  THEN SPACE ;

PERIOD and ZERO simply display one character each. ZEROS displays a given number of zeros, just like SPACES displays a number of spaces. -TRAILINGZEROES is similar to -TRAILING. An optional sign character is displayed by .SIGN. The number of significant digits for pictured numeric output of floating-point numbers is controlled by the value PRECISION. By default, StrongForth displays up to 16 digits. The maximum is 18, but with 18 significant digits rounding errors become visible even after simple arithmetic operations.

. starts with converting a floating-point number into the number of desired digits. The transient area that is used for numeric output of integers is the character buffer. Four cases have to be considered:

  1. The integer part of the absolute value of the number has more digits than specified by PRECISION. . displays an optional sign, all significant digits, the required number of zeros, and a decimal point.
  2. The integer part of the absolute value of the number has less digits than specified by PRECISION, but it's greater than or equal to 1. . displays an optional sign, the digits that belong to the integer part, a decimal point, and then the digits that belong to the decimal fraction.
  3. The absolute value of the number is less than 1. . displays an optional sign, a zero, a decimal point, the required number of zeros, and finally the significant digits.
  4. The number is not valid. . displays the character string provided by REPRESENT after removing trailing spaces.

Note that the transient area used by . is only 34 character long. Other memory areas might get overwritten if the representation of the floating-point number requires more than 34 characters, which at the maximum possible precision can happen if the number is either

Such an overflow of the transient area cannot happen if floating-point numbers are displayed in exponent representation using S. or E.. Again, StrongForth removes the "F" prefix from the respective ANS Forth words. The exponent is always displayed with a sign character (even if it is positive) and three digits:

: .SIGN+ ( FLAG -- )
  IF [CHAR] - ELSE [CHAR] + THEN . ;

: .EXPONENT ( SIGNED -- )
  [CHAR] E . DUP 0< .SIGN+
  ABS +999 MIN S>D <# # # # #> TYPE ;

S. and E. both use (SE.) to display a floating-point number in exponent representation. (SE.) has an additional parameter of data type SIGNED that is used to adjust the exponent. The exponent is always a multiple of SIGNED, and the number of digits to the left of the decimal point is between 1 and SIGNED. For engineering represenation, SIGNED is +3:

: (SE.) ( FLOAT SIGNED -- )
  LOCALS| M | TRANS-BOTTOM PRECISION REPRESENT
  IF .SIGN TRANS-BOTTOM @ [CHAR] 0 = IF 1+ THEN
     DUP 1- M MOD DUP 0< IF M + THEN 1+ TO M
     TRANS-BOTTOM PRECISION OVER M CAST UNSIGNED TYPE
     PERIOD M /STRING TYPE M - .EXPONENT
  ELSE DROP DROP TRANS-BOTTOM PRECISION -TRAILING TYPE
  THEN SPACE ;

: S. ( FLOAT -- )   +1 (SE.) ;
: E. ( FLOAT -- )   +3 (SE.) ;

Finally, let's view some examples:

123456E-7 DUP S. DUP E. .
1.234560000000000E-002 12.34560000000000E-003 0.0123456  OK
123456E-6 DUP S. DUP E. .
1.234560000000000E-001 123.4560000000000E-003 0.123456  OK
123456E-5 DUP S. DUP E. .
1.234560000000000E+000 1.234560000000000E+000 1.23456  OK
123456E-4 DUP S. DUP E. .
1.234560000000000E+001 12.34560000000000E+000 12.3456  OK
123456E+0 DUP S. DUP E. .
1.234560000000000E+005 123.4560000000000E+003 123456.  OK
123456E+1 DUP S. DUP E. .
1.234560000000000E+006 1.234560000000000E+006 1234560.  OK
123456E+2 DUP S. DUP E. .
1.234560000000000E+007 12.34560000000000E+006 12345600.  OK
-123456E+3 DUP S. DUP E. .
-1.234560000000000E+008 -123.4560000000000E+006 -123456000.  OK
1E0 0E0 / DUP S. DUP E. .
INFINITY INFINITY INFINITY  OK

FDEPTH

One ANS Forth word that is not available in StrongForth is FDEPTH. Since DEPTH returns the depth of the data type heap instead of the depth of the data stack, floating-point numbers are actually included in DEPTH. It would have been inconsistet defining a version of FDEPTH next to DEPTH, with FDEPTH referring to the hardware floating-point stack, but DEPTH referring to the data type heap instead of to the data stack. If you need these two words with ANS sematics anyway, you can easily define them:

: ANS-DEPTH ( -- SIGNED )
  SP@ -> SINGLE SP0 -> SINGLE SWAP - ;
 OK
HEX
 OK
: ANS-FDEPTH ( -- SIGNED )
  4100 DUP FXAM AND = IF +0 ELSE +8 FP@ - THEN ;
 OK
DECIMAL
 OK
CHAR S 1000000. TRUE -4.5E3 PI 5 BIT .S
CHARACTER UNSIGNED-DOUBLE FLAG FLOAT FLOAT LOGICAL  OK
ANS-DEPTH .
5  OK
ANS-FDEPTH .
2  OK

Dr. Stephan Becher - December 29th, 2007