Simply FPU Chap. 13

SIMPLY FPU
by Raymond Filiatreault
Copyright 2003

Chap. 13
Commented example

The example presented in this chapter is based on a simple dialog box to compute the monthly payments on a mortgage based on the following formula:

where:
P = Mortgage Principle
R = Monthly Interest Rate (expressed in decimal)
N = Number of months
PMT = Computed monthly payments

The dialog box itself was designed with the "WYSIWYG" feature of the Symantec Resource Editor Rs32.exe provided with the MASM32 package in the rc sub-folder. The only manual modification to the generated script was to add the ES_NUMBER style to two of the edit controls, that style not being an option with Rs32. The resource script is reproduced at the end of this document.

Three separate edit controls are provided for the user to input the mortgage principal, the annual interest rate (expressed as a %) and the number of years. Two radio buttons are also provided to choose between two procedures for computing monthly payments.

One procedure is based on the generally allowed practice in the USA of compounding interest on a monthly basis. For example, a 12% stated annual rate becomes a 1% monthly rate which, when compounded, is equivalent to a true 12.6825% annual rate.

The other procedure is based on a restriction imposed on Canadian chartered banks for mortgage loans where the stated annual rate cannot be compounded more than twice per year. For example, a 12% stated annual rate becomes a 6% semi-annual rate which, when compounded, is equivalent to a true 12.36% annual rate. The monthly rate must thus be computed according to the following formula where RSA is the semi-annual rate:

R = (1+RSA)1/6 - 1

When this computed monthly rate is compounded on a monthly basis, it would be equivalent to the semi-annual rate.

In order to simplify the program as much as possible, some limits have been imposed on the user input but may not even be noticed.

-- The input for the mortgage principal is restricted to numbers only (with the ES_NUMBER style for its edit control) and limited to a maximum of 9 characters (with the EM_SETLIMITTEXT message during the WM_INITDIALOG phase). This still allows for a mortgage input of up to 1 billion dollars which should generally be sufficient, but disallows the inclusion of pennies (which would be very rarely specified anyway) as part of the input. The main advantage of those restrictions is that it guarantees a positive binary 32-bit integer can be retrieved directly from the edit control without any need to parse the input for invalid characters and perform the conversion from ASCII with additional code.

-- The input for the number of years is also restricted to numbers only and limited to a maximum of 2 characters. This still allows for a mortgage life of up to 99 years, but disallows partial years (which is also rarely specified) as part of the input. Advantages are the same as above.

-- The input for the annual rate is restricted to 9 characters (rates are rarely specified with more than 3 decimals). This, however, guarantees that the value of the numerical digits excluding the decimal delimiter would not exceed the maximum possible value of a positive 32-bit integer.

While parsing the annual rate, a few more restrictions generate an error message:

-- An annual rate exceeding 100% (which would be considered illegal loan-sharking). This guarantees that the log2 of any (1+R) term will always be less than 1.

-- A "-" sign. The rate must be positive.

A lack of input or an input equal to 0 in any of the three edit controls also generates an error message and no computation is performed.

With pre-validated data, the computation can thus proceed without any risk of error. Some code has nevertheless been added before displaying the result to ascertain that no major problem has been encountered due to unforeseeable circumstances.

The code has been kept as simple as possible, without any attempt to optimize it for speed or size (speed optimization would be a waste of time and effort for such an application). It is also specific for the application. Even the code to convert the annual rate from a string to a floating point should not be generalized without modifying the parts which rely on the designed purpose and restrictions.

The provided code is fully tested. It can be assembled without modification with MASM32 if copied into a file with the .asm extension. If the resource script is copied into a file named rsrc.rc and placed in the same directory as the .asm file, the .exe file can be generated in a single step with the Project->Build All menu option of the QEditor in MASM32. Modifications to the code and/or assembly procedures will be required with other assemblers and/or IDEs.

The FPU instructions used with this example application are (in alphabetical order):

    F2XM1     2 to the X power minus 1
    FADD      Add two floating point values
    FBSTP     Store BCD data to memory
    FCHS      Change the sign of ST(0)
    FDIV      Divide two floating point values
    FIDIV     Divide ST(0) by an Integer located in memory
    FILD      Load integer from memory
    FIMUL     Multiply ST(0) by an Integer located in memory
    FINIT     Initialize the FPU
    FLD       Load real number
    FLD1      Load the value of 1
    FMUL      Multiply two floating point values
    FRNDINT   Round ST(0) to an integer
    FSCALE    Scale ST(0) by ST(1)
    FSTP      Store real number and pop ST(0)
    FSTSW     Store status word
    FSUB      Subtract two floating point values
    FWAIT     Wait while FPU is busy
    FXCH      Exchange the top data register with another data register
    FYL2XP1   Y*Log2(X+1)

    ; #######################################################################
    ;
    ;                    Mortgage payment calculator
    ;            Written with MASM32 by Raymond Filiatreault
    ;                           August 2003
    ;
    ; #######################################################################

          .386                   ; minimum processor needed for 32 bit
          .model flat, stdcall   ; FLAT memory model & STDCALL calling
          option casemap :none   ; set code to case sensitive

    ; #######################################################################

          include \masm32\include\windows.inc
          include \masm32\include\user32.inc
          include \masm32\include\kernel32.inc
          include \masm32\include\comdlg32.inc
          include \masm32\include\comctl32.inc

          includelib \masm32\lib\user32.lib
          includelib \masm32\lib\kernel32.lib
          includelib \masm32\lib\comdlg32.lib
          includelib \masm32\lib\comctl32.lib

    ; #########################################################################

          return MACRO arg
            mov eax, arg
            ret
          ENDM

        WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

    ; #########################################################################

    PRINCIPLE   EQU   711
    RATEPCT     EQU   712
    YEARS       EQU   713
    PAYMENT     EQU   714
    COMPUTE     EQU   720
    QUITS       EQU   721
    USABUT      EQU   730
    CANADABUT   EQU   731
    AMERICAN    EQU   0

    .data
          hDlg        dd    0
          hInstance   dd    0

          mortgage    dd    0
          months      dd    0

          factor6     dd    6
          factor10    dd    10
          factor12    dd    12

          bcdtemp     dt    0

          radiobutton db    0           ;0=USA, 1=Canada

          badinput    db    "Input error",0
          princerr    db    "Unacceptable input for principle",0
          raterr      db    "Unacceptable input for rate",0
          yearerr     db    "Unacceptable input for years",0
          invalid     db    "Invalid FPU operation detected",0

          buffer1     db    16 dup(0)

    ; #########################################################################

    .code

    start:

          invoke GetModuleHandle,NULL
          mov    hInstance,eax
          invoke InitCommonControls

          invoke DialogBoxParam,hInstance,700,NULL,ADDR WndProc,NULL

          invoke ExitProcess,eax

    ; #########################################################################

    WndProc proc hWin   :DWORD,
                 uMsg   :DWORD,
                 wParam :DWORD,
                 lParam :DWORD

          .if uMsg == WM_INITDIALOG
                push  hWin
                pop   hDlg        ;save handle in a global variable
                                  ;this avoids having to pass it as a
                                  ;parameter whenever needed outside this proc

                invoke SendDlgItemMessage,hDlg,PRINCIPLE,EM_SETLIMITTEXT,9,0
                invoke SendDlgItemMessage,hDlg,RATEPCT,EM_SETLIMITTEXT,9,0
                invoke SendDlgItemMessage,hDlg,YEARS,EM_SETLIMITTEXT,2,0
                invoke CheckRadioButton,hDlg,USABUT,CANADABUT,USABUT
                return TRUE

          .elseif uMsg == WM_COMMAND
                mov     eax,wParam
                and     eax,0ffffh

                .if   eax == QUITS            ;Exit button clicked
                      invoke  EndDialog,hDlg,0

                .elseif eax == COMPUTE        ;Compute button clicked
                      call  maincalc          ;process input and display result
                      return  TRUE

                .elseif eax == USABUT         ;USA radiobutton clicked
                      mov   radiobutton,0     ;store info
                      return  TRUE

                .elseif eax == CANADABUT      ;Canada radiobutton clicked
                      mov   radiobutton,1     ;store info
                      return  TRUE
                .endif

          .elseif uMsg == WM_CLOSE
                invoke  EndDialog,hDlg,0

          .endif

          return FALSE        ;use Windows defaults to handle other messages

    WndProc endp

    ; ########################################################################

    maincalc:

    ;*********************************************************
    ;*****     Retrieve THE MORTGAGE PRINCIPAL input     *****
    ;*********************************************************

          invoke GetDlgItemInt,hDlg,PRINCIPLE,0,0       ;retrieve as an integer
          mov   mortgage,eax      ;store it
          or    eax,eax           ;check if input > 0
          ja    @F                ;jump if OK
          lea   eax,princerr      ;error message for input of principal

    inputerror:
          invoke  MessageBox,0,eax,ADDR badinput,MB_OK  ;display error message
          ret

    ;******************************************************
    ;*****     Retrieve the NUMBER OF YEARS input     *****
    ;******************************************************

       @@:
          invoke GetDlgItemInt,hDlg,YEARS,0,0           ;retrieve as an integer
          mul   factor12          ;convert it to months
          mov   months,eax        ;store it
          or    eax,eax           ;check if input > 0
          ja    @F                ;jump if OK
          lea   eax,yearerr       ;error message for input of years
          jmp   inputerror

    ;**************************************************
    ;*****     Retrieve the ANNUAL RATE input     *****
    ;**************************************************

       @@:
          invoke GetDlgItemText,hDlg,RATEPCT,ADDR buffer1,16 ;retrieve as string
          .if   buffer1 == 0      ;buffer1 being a global BYTE variable
                                  ;if there was no input in the EDIT control,
                                  ;the first byte of buffer1 would be 0

    badrate:
              lea   eax,raterr    ;error message for annual rate input
              jmp   inputerror
          .endif

          call  atofl             ;convert ASCII string (%) to REAL10 decimal
                                  ;->st(0)=decimal annual rate (if no error)

                ; Note: The "atofl" sub-routine is called only once
                ; and its code could have been inserted directly
                ; without the need for the "call" instruction. This
                ; was done for the purpose of clarity in this section.

          or    eax,eax           ;check if error was detected
          jz    badrate           ;abort and display message if error

    ;*************************************************************
    ;*****     Convert the annual rate to a MONTHLY rate     *****
    ;*************************************************************

          .if   radiobutton == AMERICAN
              fidiv factor12      ;divide the annual rate by 12
                            ;-> st(0)=monthly rate

          .else
              fld1          ;-> st(0)=1, st(1)=annual rate
              fchs          ;-> st(0)=-1, st(1)=annual rate
              fxch          ;-> st(0)=annual rate, st(1)=-1 scaling factor
              fscale        ;divide the annual rate by 2^1
                            ;to get the semi-annual rate
                            ;(a positive scaling factor will multiply
                            ;a negative scaling factor will divide)
              fstp  st(1)   ;overwrite the scaling factor with st(0)
                            ;and pop the top register
                            ;-> st(0)=semi-annual rate
              fld1          ;-> st(0)=1, st(1)=semi-annual rate
              fidiv factor6 ;-> st(0)=1/6, st(1)=semi-annual rate
              fxch          ;-> st(0)=semi-annual rate, st(1)=1/6
              fyl2xp1       ;-> st(0)=[log2(semi-annual rate+1)]*1/6

              ;Note: Because of limitations imposed on input for size and sign,
                    ;the (semi-annual rate+1) term will be between 1 and 1.5
                    ;and its log2 will be positive and less than 1. That log2
                    ;is further divided by 6 for a value definitely between
                    ;0 and +1. That value can thus be used directly with
                    ;the next instruction without any need for scaling before
                    ;or after.

              f2xm1         ;-> st(0)=monthly rate (obtained directly
                            ;because the instruction provides the minus 1)
                            ;When this monthly rate is compounded 6 times
                            ; it would be equal to the semi-annual rate
          .endif

    ;************************************************
    ;*****     Compute the monthly payments     *****
    ;************************************************

          fild  months      ;-> st(0)=months, st(1)=monthly rate (R)
          fld   st(1)       ;-> st(0)=R, st(1)=months, st(2)=R
          fyl2xp1           ;-> st(0)=log2(1+R)*months, st(1)=R
          fld   st          ;-> st(0)=log, st(1)=log, st(2)=R
          frndint           ;-> st(0)=int(log), st(1)=log, st(2)=R
          fsub  st(1),st    ;-> st(0)=int(log), st(1)=log-int(log), st(2)=R
          fxch  st(1)       ;-> st(0)=log-int(log), st(1)=int(log), st(2)=R
          f2xm1             ;-> st(0)=2[log-int(log)]-1, st(1)=int(log), st(2)=R
          fld1
          fadd              ;-> st(0)=2[log-int(log)], st(1)=int(log), st(2)=R
          fscale            ;-> st(0)=(1+R)N, st(1)=int(log), st(2)=R
          fstp  st(1)       ;-> st(0)=(1+R)N, st(1)=R
          fld   st          ;-> st(0)=(1+R)N, st(1)=(1+R)N, st(2)=R
          fld1              ;-> st(0)=1, st(1)=(1+R)N, st(2)=(1+R)N, st(3)=R
          fsub              ;-> st(0)=(1+R)N-1, st(1)=(1+R)N, st(2)=R
          fdiv              ;-> st(0)=(1+R)N/[(1+R)N-1], st(1)=R
          fmul              ;-> st(0)=R*(1+R)N/[(1+R)N-1]
          fimul mortgage    ;-> st(0)=P*R*(1+R)N/[(1+R)N-1]=Monthly payments
          fimul factor10    ;multiply by 100 to have 2 decimal places as integer
          fimul factor10    ;-> st(0)=Monthly payments*100
          fbstp bcdtemp     ;store in memory in BCD format
                            ;rounded to the closest penny

    ;*************************************************************************
    ; The folowing section of code is not necessary for this application
    ; because every precaution was taken to examine the input to insure that
    ; the data used in the FPU computations is valid and would not result in
    ; any major error. It is merely included to indicate how to check the
    ; validity of the end result whenever there may be a risk of invalid data.
    ; It also insures that the FBSTP instruction is completed before starting
    ; to access the stored packed BCD data.
    ;*************************************************************************

          fstsw ax          ;copy to AX the FPU's Status Word
                            ;containing the exception flags
          fwait             ;insure the execution is completed
          shr   eax,1       ;transfer bit0 to the CPU's carry flag
                            ;That bit would be set if an invalid operation
                            ;was detected with any of the FPU instructions
                            ;The final result would then be invalid
          jnc   @F          ;continue if no invalid operation flag

          lea   eax,invalid
          jmp   inputerror  ;display error message and return to WndProc

    ;****************************************************************
    ;*****         Unpack the BCD result and display it         *****
    ;
    ; The coding used is not suitable as a general purpose unpacking
    ; algorithm for at least two reasons: the sign is known to be
    ; positive and thus disregarded, and the result was designed to
    ; always contain 2 decimal places.
    ;***************************************************************

       @@:
          push  esi               ;preserve ESI and EDI
          push  edi
          lea   esi,bcdtemp+8     ;use ESI to point initially
                                  ;to the 2nd most significant byte
          lea   edi,buffer1       ;use EDI to point to the buffer
                                  ;where the ASCII string will be stored
          mov   ecx,8             ;use ECX as counter for the number of bytes
                                  ;possibly containing integer digits

                ;Note: (The BCD format has 10 bytes. The most significant
                      ;byte contains the sign which is disregarded in this
                      ;application, and the least significant byte is known
                      ;to contain two decimal digits.)

    ;**********************************************************************
    ; Search for the most significant byte containing the 1st integer digit
    ;**********************************************************************

       @@:
          movzx eax,byte ptr[esi] ;get next byte in AL zero extended in EAX
          dec   esi         ;adjust pointer to next byte
          or    eax,eax     ;check if it contains the 1st integer
          jnz   @F          ;jump if found
          dec   ecx         ;decrement counter
          jnz   @B          ;continue search until all integer bytes checked

    ;***********************************************
    ; If no integer digit is present,
    ; place a "0" digit before the decimal delimiter
    ;***********************************************

          mov   al,"0"
          stosb             ;insert the "0" digit
          jmp   decimals    ;go insert the decimal delimiter and digits

    ;*******************************************************
    ; If that 1st byte contains only 1 integer digit,
    ; it has to be processed separately to avoid a leading 0
    ;*******************************************************

       @@:
          test  al,0f0h     ;check the high nibble of AL
          jnz   @F          ;jump if there are 2 integer digits
          add   al,"0"      ;convert the digit to ASCII
          stosb             ;insert that digit
          jmp   nextdigit   ;process next byte

    ;*******************************************************************
    ; Other bytes contain 2 integer digits each which must be unpacked
    ; The high nibble digit must be followed in memory by the low nibble
    ;*******************************************************************

       @@:
          ror   ax,4              ;transfer the high nibble to low nibble of AL
                                  ;and low nibble of AL to high nibble of AH
          ror   ah,4              ;transfer it to the low nibble of AH
          add   ax,3030h          ;convert both to ASCII
          stosw                   ;insert both, AL followed by AH in memory

    nextdigit:
          movzx eax,byte ptr[esi] ;get next byte
          dec   esi               ;adjust pointer
          dec   ecx               ;decrement integer byte counter
          jnz   @B                ;continue with integer digits until completed

    decimals:
          mov   byte ptr[edi],"." ;insert the decimal delimiter
          inc   edi               ;adjust string pointer
          ror   ax,4              ;unpack the decimal byte as above
          ror   ah,4
          add   ax,3030h
          stosw
          mov   byte ptr[edi],0   ;insert the terminating 0

          pop   edi               ;restore the EDI and ESI registers
          pop   esi

    ;***********************************************************************
    ; Display the result in the dialog box and return control to the WndProc
    ;***********************************************************************

          invoke SetDlgItemText,hDlg,PAYMENT,ADDR buffer1
          ret

    ;***************************************************************
    ;                            atofl
    ;
    ; This sub-routine is partly general purpose and partly specific
    ; for the task. It parses the string for unacceptable characters
    ; but also returns an error if the integral portion of the input
    ; exceeds 100. It also returns an error for a negative sign.
    ;
    ; The conversion approach of treating all the numerical digits as
    ; being integer digits is sound only because the size of the
    ; string was limited to 9 characters in the EDIT control. This
    ; guarantees that, in a worse case scenario, the maximum integer
    ; value of the input would still fit in a 32-bit register. The
    ; final integer value obtained is then corrected according to the
    ; count of decimal digits in the input string.
    ;
    ; Returns with EAX = 0 if error detected.
    ;
    ; If EAX != 0, the converted annual rate is returned in st(0)
    ; already divided by 100.
    ;****************************************************************

    atofl:
          push  ebx         ;preserve EBX and ESI
          push  esi

          lea   esi,buffer1 ;use ESI as pointer to text buffer
          xor   eax,eax
          xor   ebx,ebx     ;will be used as an accumulator
          xor   ecx,ecx     ;will be used as a counter

    ;************************************************
    ; Skip leading spaces without generating an error
    ;************************************************

       @@:
          lodsb             ;get next character
          cmp   al," "      ;check if a space character
          jz    @B          ;repeat until a non-space character is found

    ;*********************************************
    ; Check 1st non-space character for a +/- sign
    ;*********************************************

          cmp   al,"-"      ;is it a "-" sign
          jnz   @F

    atoflerr:
          xor   eax,eax     ;set EAX to error code
          pop   esi         ;restore the EBX and ESI registers
          pop   ebx
          ret               ;return with error code

       @@:
          cmp   al,"+"      ;is it a "+" sign
          jnz   nextchar
          lodsb             ;disregard a "+" sign and get next character

    ;***********************************************************
    ; From this point, space and sign characters will be invalid
    ;***********************************************************

    nextchar:
          cmp   al,0        ;check for end-of-string character
          jz    endinput    ;exit the string parsing section

          cmp   al,"."      ;is it the "." decimal delimiter
                            ;other delimiters such as the "," used in some
                            ;countries could also be allowed but would need
                            ;additional coding to make it more generalized
          jnz   @F

    ;******************************************************************
    ; Only one decimal delimiter can be acceptable. The sign bit of ECX
    ; is used to keep a record of the first delimiter identified.
    ;******************************************************************

          or    ecx,ecx     ;check if a delimiter has already been identified
          js    atoflerr    ;exit with error code if more than 1 delimiter

          stc               ;set the carry flag
          rcr   ecx,1       ;set bit31 of ECX (the sign bit) when
                            ;the 1st delimiter is identified
          lodsb             ;get next character
          jmp   nextchar    ;continue parsing

    ;***********************************************************************
    ; All ASCII characters other than the numerical ones will now be invalid
    ;***********************************************************************

       @@:
          cmp   al,"0"
          jb    atoflerr
          cmp   al,"9"
          ja    atoflerr

          sub   al,"0"      ;convert valid ASCII numerical character to binary
          xchg  eax,ebx     ;get the accumulated integer value in EAX
                            ;holding the new digit in EBX
          mul   factor10    ;multiply the accumulated value by 10
          add   eax,ebx     ; and add the new digit
          xchg  eax,ebx     ;store this new accumulated value back in EBX

          or    ecx,ecx     ;check if a decimal delimiter detected yet
          js    @F          ;jump if decimal digits are being processed

    ;*************************************
    ; Integer digits still being processed
    ;*************************************

          cmp   ebx,100     ;verify current value of integer portion
          ja    atoflerr    ;abort if input for annual rate is > 100%
          lodsb             ;get next string character
          jmp   nextchar    ;continue processing string characters

    ;*******************************************************
    ; The CL register is used as a counter of decimal digits
    ; after the decimal delimiter has been identified
    ;*******************************************************

       @@:
          inc   cl          ;increment count of decimal digits
          lodsb             ;get next string character
          jmp   nextchar    ;continue processing string characters

    ;***********************************
    ; Parsing of the string is completed
    ;***********************************

    endinput:
          or    ebx,ebx     ;check if total input was equal to 0
          jz    atoflerr    ;abort if annual rate input is 0%

          finit             ;initialize FPU
          push  ebx         ;store value of EBX on stack
          fild  dword ptr[esp]    ;-> st(0)=EBX
          add   cl,2        ;increment the number of decimal digits
                            ;to convert from % rate to a decimal rate
          shl   ecx,1       ;get rid of the potential sign "flag"
          shr   ecx,1       ;restore the count of decimal digits
          fild  factor10    ;-> st(0)=10, st(1)=EBX
       @@:
          fdiv  st(1),st    ;-> st(0)=10, st(1)=EBX/10
          dec   ecx         ;decrement counter of decimal digits
          jnz   @B          ;continue dividing by 10 until count exhausted
          fstp  st          ;get rid of the dividing 10 in st(0)
                            ;-> st(0)=annual rate (as a decimal rate)
          pop   ebx         ;clean CPU stack

          pop   esi         ;restore the EBX and ESI registers
          pop   ebx
          or    al,1        ;insure EAX != 0 (i.e. no error detected)
          ret

    ;*******************************

    end start

    ;#######################################################################
    ;
    ;                    Resource script for the Dialog Box
    ;                 to be used with the Mortgage Calculator
    ;                               August 2003
    ;
    ;#######################################################################

    #include "\masm32\include\resource.h"

    700 DIALOGEX MOVEABLE IMPURE LOADONCALL DISCARDABLE	100, 80, 131, 132, 0
    STYLE DS_MODALFRAME | 0x0004 | WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP
    CAPTION "Mortgage Payments"
    FONT 8, "MS Sans Serif", 700, 0 /*FALSE*/
    BEGIN
        LTEXT     "Mortgage principle", 701, 4,6,66,10, SS_LEFT, , 0
        LTEXT     "Annual rate, %", 702, 	4,22,66,10, SS_LEFT, , 0
        LTEXT     "Number of years", 703, 	4,38,66,10, SS_LEFT, , 0
        LTEXT     "Monthly payments", 704, 	4,84,66,10, SS_LEFT, , 0
        EDITTEXT  711,  78, 6,49,11, ES_RIGHT | ES_NUMBER, , 0
        EDITTEXT  712,  78,22,49,11, ES_RIGHT, , 0
        EDITTEXT  713, 102,38,25,11, ES_RIGHT | ES_NUMBER, , 0
        CONTROL   "", 714, "Edit", ES_READONLY | ES_RIGHT, 	78,84,49,11, , 0
        CONTROL   "Compute", 720, "Button", BS_DEFPUSHBUTTON, 16,111,41,13, , 0
        CONTROL   "Exit", 721, "Button", 0, 	74,111,41,13, , 0
        CONTROL   "U.S.A.", 730, "Button", BS_AUTORADIOBUTTON | WS_GROUP, 18,62,44,10, ,0
        CONTROL   "Canada", 731, "Button", BS_AUTORADIOBUTTON, 75,62,44,10, , 0
    END

RETURN TO
SIMPLY FPU
CONTENTS