1-7  DEBUGGING AND PROGRAM VALIDATION 
 *************************************

 (Thanks to Sergio Gelato for the good comments and wealth of information,
  thanks to Dan Pop for the important information, and to Craig Burley for
  the helpful comments)

    +----------------------------------------------------------+
    |  THE BEST WAY TO DEBUG A PROGRAM IS TO MAKE NO MISTAKES  |
    +----------------------------------------------------------+

 Using compiler options
 ----------------------
 If you write programs in the 'right' way, you will have few syntax
 errors, a good compiler (called with the right options) will flag 
 them for you, and you will correct them easily.

 It is important to emphasize that most compilers are rather lenient 
 by default, and must be invoked with special options in order to get 
 many useful warnings. 

 Some of these compiler options enable compile-time checks 
 such as: 

    1) Disabling implicit type declarations 
    2) Flagging standard violations

 Others enable various run-time checks such as: 

    1) Checking array bounds 
    2) Trapping floating-point exceptions 
    3) Special variable initializations that are 
       supposed to produce floating-point exceptions 
       if uninitialized variables are used. 

 It is recommended that you create an alias (VMS global symbol) as a 
 shorthand for these long switch sequences.


         Recommended compiler options for debugging
    ============================================================
    FORTRAN/WARNING=ALL/STANDARD/CHECK=ALL              (VMS)
    f77  -u -ansi -fnonstd -C -XlistE -Xlisto /dev/tty  (SunOS)
    f77  -u -w0 -strictIEEE -trapuv -C                  (IRIX)
    f77  -u -std -fpe4 -check format \                  (DUNIX)
            output_conversion overflow underflow \
            -automatic -C                                
    f77  -u -std -fp4 -check overflow \                 (ULTRIX)
            underflow -automatic -C                      
    cf77 -Wf-enih-m0                                    (UNICOS)
    xlf  -C -qflttrap=inv:ov:zero:en:imp                (AIX)
    f77  -C +FPVZOuiD                                   (HP-UX) 


    A note on the SunOS compiler
    ----------------------------
    The Xlist option triggers global program checking. While there
    is no way to just toggle the checking, one can use:

	   f77 $basicopts -XlistE -Xlisto /dev/tty  

    To get just the errors sent to the terminal, and:

                      -Xlistwar320 -Xlistwar315 -Xlistwar314 
                      -Xlistwar357 -Xlistwar359 -Xlistwar391
                      -Xlistwar338 -Xlistwar355 -Xlistwar380 
                      -Xlistwar381 -Xlistwar205 -Xlistwar370

    To turn off the messages that you will probably want to turn off.


    A note on the xlf compiler
    --------------------------
    In some cases, it may be appropriate to use -qextchk as well. 
    This outputs extra information about the types of subprogram 
    arguments that the binder can then check at link time. 

    Occasionally one may need to stretch the rules about the types 
    of arguments (a routine may expect a COMPLEX array, but the 
    caller may have declared the array as REAL (2,*)), in which 
    case -qextchk may have to be omitted.

    Current versions (3.1 and later) of XL Fortran also support the 
    (recommended) -qlanglvl=77std, -qlanglvl=90std, -qlanglvl=90pure. 
    Choose the one that is most appropriate to your code. When writing 
    new code, try to have no warnings with -qlanglvl=90pure.


    A note on the HP-UX f77 compiler
    --------------------------------
    The recommended options for f77 are: 

       -C          (bounds checking, done at compile time)
       +FPVZOuiD   (preferred FP options, done at link time)

    The preferred FP options are: trap overflow, divide by zero, 
    invalid operand, allow abrupt underflow if supported by the 
    hardware; ignore inexact and underflow faults.

    You may have to say -Wl,+FPVZOuiD if you link with the f77 command.


    A note on the VMS compiler
    --------------------------
    The VMS options here are an overkill, and may produce unneeded error
    messages


    A note on the UNICOS f77 compiler
    ---------------------------------
    UNICOS switches may not work when running parallel.



 Using list files for correcting syntax errors
 ---------------------------------------------
 When compiling the compiler throws rapidly a list of errors at you,
 being out of context they are hard to understand, and even harder
 to remember when you return to the editor.

 Using a windowing terminal is helpful, but you can do nicely without
 one. Ask the compiler to generate a LIST FILE, then call your
 editor and put the list file in one EDITING BUFFER and your program
 in another. 

 See the compiler options chapter for a table of compiler switches
 on various operating systems.

 The list file may have the same name as the source file, with FILE
 EXTENSION  '.l',  '.L'  or  '.LIS'.


 Static code analyzers
 ---------------------
 There are static tools that can help you flush 'deeper' bugs. The
 Fortran FAQ is a good place to look for them. 

 For example, you can get FTNCHEK by anonymous ftp from netlib (see 
 the section on getting useful routines), and install it easily. 
 It is supposed to be rather primitive compared with other products, 
 but you may be surprised at the results. Try the following 
 switches:

    Purpose              Switches
   ---------            --------------------------------------
   More info            -calltree -crossref -reference -symtab
   Portability          -portability -f77 -sixchar
   Declarations         -declare
   Numerics             -division
   Common blocks        -volatile
   List file            -list

 Other interesting checks are performed by default.
 The '-' character serves as a switch prefix on both VMS and UNIX.

 It is sometimes useful to turn off an option that produces too many 
 spurious warnings, e.g. -notruncation -nopure, and occasionally even 
 -nopretty.



 Preparing for a testing
 -----------------------
 Then comes the real thing: you will have to test your program, and
 find the errors that are the result of faulty reasoning - the program 
 logic errors.

 Check again all code before testing, NOT immediately after you wrote 
 it, but when your mind is clear. You should then find all those little 
 slips of mind.

 If you didn't do it already now is the last time to apply defensive 
 programming, a good and easy way is to check all variables upon 
 entering a procedure for obvious errors, e.g. variables that should 
 be positive/non-zero (array indexes, string lengths, etc) should be 
 checked and a warning issued if they fail the test.

 Data input is best done inside special routines, so the above rule
 covers that case also, but of course the check should be done upon
 exiting the routine.

 If possible, it's recommended that you test the more complex routines 
 separately, i.e. with a suitable driver - a simple main program that
 produces the correct input for the routine, and helps you analyze
 the resulting output. The extra work involved in preparing the driver
 is worth it, debugging is much easier when you don't have to take
 into consideration complex inter-routine interactions.


 Testing the program
 -------------------
 Whenever possible, routines should be tested individually for conformance
 to their specification. Prepare a test suite that probes the working of
 the various interesting combinations of inputs, trying to exercise all
 possible execution paths within the routine.

 A good way to test a complete program is to have a 'verbose mode' in your 
 program, a 'mode' that writes to a FILE all key results.

 You get into this mode when you supply a special value (say 1) to an 
 input variable called for example 'DEBUG'. The DEBUG variable is passed 
 to ALL subprograms, in every 'interesting' place the value of DEBUG is 
 checked and if it equals 1 you write to file the last result.

 Some programmers use different 'debug levels', e.g. (DEBUG .EQ. 1) 
 writes part of the information, (DEBUG .EQ. 2) writes more info, etc.

 Some programmers develop a 'subsystem classification'. Each value of 
 the DEBUG variable corresponds to another part of the program, when
 you pass the value assigned to some subsystem, all info relevant to
 this subsystem will be written to the 'debug file'.

 Verbose mode statements can be easily removed in the final version of 
 the program, if you put them inside 'preprocessor if statements', 
 like this cpp example:

#ifdef DEBUG
      IF (DEBUG .EQ. 1) WRITE(*,*) ' X= ', X
#endif

 If you define DEBUG to the preprocessor, the FORTRAN 'if' statement 
 will be compiled, otherwise it will not.

 A remark:  "#ifdef" and "#endif" are nonstandard, but can usually
            be supported on almost any system, since C preprocessors 
            are nearly ubiquitous at this point. See the chapter on
            preprocessors.

 Another way is to use the non-standard 'd lines' (debug lines) feature, 
 some compilers can be instructed with a compiler switch to ignore or use 
 lines that begin with the letter 'D' (on the first column). 

   +--------------------------------------------------------------------+
   |  TRY AS DIFFERENT AS POSSIBLE INPUTS IN VERBOSE MODE, AND LOOK     |
   |       CLOSELY AT THE RESULTS WRITTEN IN THE DEBUG FILE             |
   |                                                                    |
   |  IF POSSIBLE, COMPARE RESULTS WITH OTHER RESULTS KNOWN TO BE GOOD  |
   +--------------------------------------------------------------------+


 Tracing bugs
 ------------
 The first rule about debugging is staying cool, treat the misbehaving
 program as an intellectual exercise, ignore schedules and bosses.

 1) You got some strange results that made you think there is a bug. 
    Think again, are you sure they are not the correct output for 
    some special input?

 2) If you are not sure what causes the bug, DON'T try semi-random
    code modifications, that's seldom works. Your aim should be to
    gather as much as possible information!

 3) If you have a modular program, each part does a clearly defined 
    task, so properly placed 'debug statements' can ISOLATE the 
    malfunctioning procedure/code-section. 

 4) If you are familiar with a debugger use it, but be careful not
    to be carried away by the many options and start playing. 


 Some common pitfalls
 --------------------
   Data-types
   ==========
    1) Using implicit variable declarations
    2) Using a non-intrinsic function without a type declaration
    3) Incompatible argument lists in CALL and the routine definition
    4) Incompatible declarations of a common block
    5) Using constants and parameters of incorrect (smaller) type
    6) Assuming that untyped integer constants get typed properly
    7) Assuming intrinsic conversion-functions take care of result type

   Arithmetic
   ==========
    1) Using constants and parameters of incorrect type
    2) Uncareful use of automatic type promotions
    3) Assuming that dividing two integers will give a floating-point 
       result (Like in Pascal, where there is a special operator for
       integer division)
    4) Assuming integer exponents (e.g. 2**(-3)) are computed as 
       floating-point numbers
    5) Using floating-point comparisons tests, .EQ. and .NE. are 
       particularly risky
    6) Loops with a REAL or DOUBLE PRECISION control variable
    7) Assuming that the MOD function with REAL arguments is exact
    8) Assuming real-to-integer assignment will work in all cases
       
   Miscellaneous
   =============
    1) Code lines longer than the allowed maximum
    2) Common blocks losing their values while the program runs
    3) Aliasing of dummy arguments and common block variables, 
       or other dummy arguments in the same subprogram invocation
    4) Passing constants to a subprogram that modifies them 
    5) Bad DO-loop parameters (see the DO loops chapter)
    6) TABs in input files - what you see is not what you get!

   General
   =======
    1) Assuming variables are initialized to zero 
    2) Assuming variables keep their value between the execution 
       of a RETURN statement and subsequent invocations
    3) Letting array indexes go out of bounds
    4) Depending on assumptions about the computing order of subexpressions
    5) Assuming Short-circuit evaluation of expressions
    6) Using trigonometric functions with large arguments on some machines
    7) Inconsistent use of physical units


 Remarks on the pitfalls list
 ============================
    See the chapter on FORTRAN pitfalls for a fuller explanation.

    Some of these pitfalls can be located by the compiler (see
    the beginning of this chapter) or a static code checker like 
    FTNCHEK, others may need carful debugging. 

    The improved syntax of Fortran 90 eliminates some of these 
    pitfalls e.g. loops with a floating-point control variable(?), 
    other pitfalls will be detected by a compiler conforming to 
    this standard.

    Note that a Fortran 90 compiler will often only be able to 
    help you if you make use of the stricter checking features 
    of the new standard: 

       1) IMPLICIT NONE 
       2) Explicit interfaces 
       3) INTENT attributes 
       4) PRIVATE attributes for module-wide data that 
          should not be accessible to the outside


Return to contents page