Skip to content

Assessing code quality with the NAG Fortran compiler

The NAG Fortran compiler, like other compilers, has diagnostic capabilities which can help us write correct and portable Fortran programs. In this post we'll look at these, comparing with those of the GCC and Intel compilers, and see how the compiler can be a valuable tool when developing or maintaining Fortran code.

Introduction

In a previous post, we saw how the GCC and Intel compilers can be asked to diagnose errors in code beyond those which they look at by default. In this post, we'll look at using the NAG Fortran compiler in a similar way, noting some additional diagnostic features to help us write standard conforming and portable Fortran code.

Error checking and diagnostics

In that previous post, there are a number of different types of errors which can be identified when using compiler flags. For the GCC and Intel Fortran compilers (gfortran and ifort, respectively), the post suggests using:

$ gfortran -Wall -fcheck=all -Og -g -fbacktrace example.f90 -o example
...
$ ifort -warn all -check all -O0 -g -traceback example.f90 -o example
...

For the NAG Fortran compiler, we have the comparable options (noting that many warnings are enabled by default with this compiler):

$ nagfor -C=all -O0 -g -gline example.f90 -o example
...

In the examples that follow, we also use the -quiet option when compiling to suppress some verbose output.

Error types from the previous post

Bounds violations and incorrect procedure references

Using these checking options, we can look for the errors in the same examples from that previous post. For example, the compiler detects array bounds violations:

$ nagfor -quiet -C=all -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 8: Subscript 1 of X (value 0) is out of range (1:10)
Program terminated by fatal error
example.f90, line 8: Error occurred in EXAMPLE
Abort

With these same options, the compiler can detect incorrect references to procedures with implicit interfaces:

$ nagfor -quiet -C=all -O0 -g -gline example.f90 set_x.f90 -o example
example.f90:
set_x.f90:
Loading...
$ ./example
Runtime Error: set_x.f90, line 1: Invalid procedure reference -
Actual argument for dummy argument X is REAL(real64) instead of REAL(real32)
Program terminated by fatal error
set_x.f90, line 1: Error occurred in SET_X
example.f90, line 7: Called by EXAMPLE
Abort

The Intel compiler's check for this type of error are performed when compiling (by automatically creating module files for external procedures as they are compiled and then using these modules to check interfaces). The NAG compiler's checks, however, take place when the procedure is referenced during the running of the program.

References to undefined variables

The checks performed using -C=all don't include detection of undefined variable references, but such detection can be enabled using -C=undefined:

$ nagfor -quiet -C=undefined -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 5: Reference to undefined variable X
Program terminated by fatal error
example.f90, line 5: Error occurred in EXAMPLE
Abort

In this example, the undefined variable is one which was not initialized and had not been given a value before it was referenced. The NAG Fortran compiler can additionally detect some cases where a variable is referenced after it becomes undefined. We can consider the following example:

program example
  implicit none

  integer :: i=0

  print '("Before setting: i=",I0)' , i
  call set(i,2)
  print '("After setting: i=",I0)' , i

contains

  subroutine set(var, val)
    integer, intent(out) :: var
    integer, intent(in)  :: val
  end subroutine set

end program example

In this case, the variable i in the main program is associated with the dummy argument var in the subroutine. The dummy argument is an intent(out) argument but, as a mistake, its value isn't set: i becomes undefined on entering the subroutine and remains undefined on completion.

$ nagfor -quiet -C=undefined -gline example.f90 -o example
Warning: example.f90, line 15: Unused dummy variable VAL
Warning: example.f90, line 15: Unused dummy variable VAR
Warning: example.f90, line 15: INTENT(OUT) dummy argument VAR never set
$ ./example
Before setting: i=0
Runtime Error: example.f90, line 8: Reference to undefined variable I
Program terminated by fatal error
example.f90, line 8: Error occurred in EXAMPLE
Abort

Equally, cases of references of a reference of an undefined variable may prompt a warning when compiling. For example, the program

program example
  implicit none

contains

  subroutine sub(i)
    integer, intent(out) :: i
    integer j

    i = j
  end subroutine sub

end program example

uses the undefined variable j in the subroutine:

$ nagfor -quiet example.f90
Warning: example.f90, line 11: Symbol J referenced but never set

This warning is issued by default, without requiring a compiler option.

The compiler's efforts at detecting references to undefined variables is useful when we are developing and debugging code. It is, however, subject to a number of limitations. In particular, the compiler won't be able to pick up all references to undefined variables and may also give false positives. Further, to use the -C=undefined option, all program units in a program must be compiled with the option, and no call to a bind(c) procedure may be made. Full details of the limitations can be found in the compiler's documentation.

Mistakes in variable names

The -u compile option acts like the -fimplicit-none and -warn declarations options offered by the GCC and Intel compilers. With this option, code is treated as having implicit none in force, allowing us to detect incorrect spelling of variable names:

$ nagfor -quiet -u example.f90
Error: example.f90, line 3: Implicit type for MYCOMP1ICATEDVARIABLE
       detected at MYCOMP1ICATEDVARIABLE@=

Additional error types

There are further types of errors that the NAG Fortran compiler can help identify in our code, beyond those of the previous post. A few examples follow but more detail is available in the compiler's documentation.

Dangling pointers

When using pointers in Fortran, it's important to ensure that they aren't dereferenced when not associated. As well as through explicit nullification or deallocation, pointers may become disassociated through other actions.

For example, in the following program, the variables i and j of the main program become pointer associated, through the dummy arguments of the subroutine. This association in the subroutine is allowed because the dummy argument j has the target attribute even though the actual argument has not. This association lasts only as long as the subroutine is being executed and is broken when the subroutine completes.

program example
  implicit none

  integer, pointer :: i => NULL()
  integer :: j = 1

  call pointer_assign(i, j)

  print*, i

contains

  subroutine pointer_assign(pointer, target)
    integer, pointer :: pointer
    integer, target :: target

    pointer => target
  end subroutine pointer_assign

end program example

Without enabling checks (also available using the option -C=dangling) for this case, the pointer may have been nullified by the subroutine, or may even still point to j or some other part of memory. In such cases, the program may crash or unexpected behaviour may occur. With the dangling pointer checks, we see that we are wrong to dereference i after the subroutine call:

$ nagfor -quiet -C=all -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 9: Reference to dangling pointer I
Target was RETURNed from procedure EXAMPLE:POINTER_ASSIGN
Program terminated by fatal error
example.f90, line 9: Error occurred in EXAMPLE
Abort

References to absent dummy arguments

Optional arguments in procedures can be a convenient way to avoid duplication of code. It's important, though, to be careful not to use an absent dummy argument inappropriately: in most cases we'd want to test for an optional argument's presence before using it.

In the following program, the dummy argument i is an optional one but the subroutine attempts to assign to it each time it is executed.

program example
  implicit none

  call sub

contains

  subroutine sub(i)
    integer, intent(out), optional :: i
    i = 1
  end subroutine sub

end program example

With runtime checking enabled (also available with -C=present), we can see that we have tried to assign to the dummy argument when it is not present:

$ nagfor -quiet -C=all -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 10: Reference to OPTIONAL argument I which is not PRESENT
Program terminated by fatal error
example.f90, line 10: Error occurred in EXAMPLE:SUB
example.f90, line 4: Called by EXAMPLE
Abort

Without such checks, we are likely to see our program crash by trying to write to a null pointer; the error message here is more explicit and more helpful in telling us where we went wrong.

Dummy argument aliasing

Inside a procedure, a compiler is allowed to make assumptions around how names refer to different areas of memory. In the subroutine axpy in the example below, the dummy argument lhs is assigned to so we are telling the compiler that lhs relates to a different part of memory from the three other arguments: it is not aliased.

The subroutine call in the main program, however, has the dummy arguments lhs and y both associated with the same y in the main program.

program example
  implicit none

  integer :: a=3, x=5, y=1

  write(*, '(I0,"*",I0," + ",I0," = ")', advance='no') a, x, y
  call axpy(y, 3, x, y) ! y = a*x + y
  print '(I0)', y

contains

  subroutine axpy(lhs, a, x, y)
    integer, intent(out) :: lhs
    integer, intent(in) :: a, x, y

    lhs = 0
    lhs = a*x + y
  end subroutine axpy

end program example

With the checks (also available with -C=alias), the compiler is able to tell us that the assignment to lhs would also affect the value of another argument:

$ nagfor -quiet -C=all  -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 16: Assignment to LHS affects dummy argument Y
Program terminated by fatal error
example.f90, line 16: Error occurred in EXAMPLE:AXPY
example.f90, line 7: Called by EXAMPLE
3*5 + 1 =
Abort

With aliasing like this, there is a possibility that the first assignment to lhs affects the value of the argument y before the latter's use in the evaluation the expression for the second assignment.

NAG Fortran's ability to detect aliasing like this is currently restricted to the case of scalar dummy arguments: see the compiler's documentation for details.

Integer overflow

When performing integer arithmetic, we may experience overflow, such as in the following program:

program example
  implicit none

  integer :: i = HUGE(i)

  print '(I0," + 1 = ",I0)', i, i+1

end program example

With the checks (also available with -C=intovf) for overflow, we are presented with an error rather than an otherwise incorrect (such as negative) result:

$ nagfor -quiet -C=all  -O0 -g -gline example.f90 -o example
$ ./example
Runtime Error: example.f90, line 6: INTEGER(int32) overflow for 2147483647 + 1
Program terminated by fatal error
example.f90, line 6: Error occurred in EXAMPLE
Abort

With the GCC compilers, similar checks are performed with the compiler option -fsanitize=signed-integer-overflow.

Diagnostics for potential mistakes

The types of error we've seen above concern violations of the Fortran language standard. In the example programs, we've broken rules of the language in a way which means that we can't guarantee how the program is executed. By requesting optional additional checks by the compiler, we've seen how we can generate diagnostic messages either when compiling or when running. Different compilers have different diagnostic abilities, with strengths and weaknesses in their own ways, and we've now seen how three compilers work to help us write correctly behaving programs.

The NAG Fortran compiler also warns us about potential mistakes in what we've written. In cases where our code does not violate the language rules but could be a mistake, we may see warnings when compiling.

We've seen some examples here already, such as when referencing undefined variables:

Warning: example.f90, line 15: Unused dummy variable VAR
Warning: example.f90, line 15: INTENT(OUT) dummy argument VAR never set

and

Warning: example.f90, line 11: Symbol J referenced but never set

The first warnings here tell us that we haven't used the dummy variable, and in particular we haven't set the value of an "output" of the procedure. We see similar warnings if we have local variables we don't use, or we set the values of variables we don't reference. For example, in the following program we have local variables i, j and k which we don't make good use of.

program example
  implicit none

contains

  subroutine sub()
    integer :: i, j, k=1
    j = 0
  end subroutine sub

end program example

When compiling, NAG Fortran warns us about these unused variables, suggesting that we may have made a mistake in our implementation, or that we can simplify our code:

$ nagfor -quiet example.f90
Warning: example.f90, line 9: Unused local variable I
Questionable: example.f90, line 9: Variable J set but never referenced
Warning: example.f90, line 9: Local variable K is initialised but never used

It is good practice for production code not to provoke these and other warnings, but they should be treated as suggestions to consider whether your code is doing what you expect; they are not errors.

Portability

The types of errors captured above may be ones where the program may crash, and the diagnostics simplify the process of finding and fixing the cause the crash. Some of the error types can lead to erroneous results if not caught, without obvious failures, and in the most unfortunate cases, only sporadically.

We ideally want to be able to compile and run our programs using many different compilers and computer systems. Not only does this make our work potentially more widely accessible, testing with many compilers allows us to take advantage of the different strengths of each.

The NAG Fortran compiler can help us detect assumptions in our programs which can restrict their portability.

Values of kind parameters and non-standard declarations

A commonly available non-standard extension for declaring the storage size of a variable is to use the form

program example
  integer*2 :: i = 1
  real*8 :: x = 1.
  real*4 :: y = 1.

  print '("Ones:", I2, 2(1X,G0.1))', i, x, y
end program example

(which relates to the standard form character*12 name). The NAG Fortran compiler accepts this form but notes it as non-standard:

$ nagfor -quiet example.f90 -o example
Non-standard(Obsolete): example.f90, line 2: Byte count on numeric data type
                        detected at *@2
Non-standard(Obsolete): example.f90, line 3: Byte count on numeric data type
                        detected at *@8
Non-standard(Obsolete): example.f90, line 4: Byte count on numeric data type
                        detected at *@4

Equally, many programs assume that the compiler uses the kind parameter to indicate the storage size. The program

program example
  implicit none

  integer(2) :: i = 1
  real(8) :: x = 1.
  real(4) :: y = 1.

  print '("Ones:", I2, 2(1X,G0.1))', i, x, y
end program example

uses literal constants for the kinds. This is generally unsafe and the NAG Fortran compiler by default does not use this numbering scheme:

$ nagfor -quiet example.f90 -o example
Error: example.f90, line 5: KIND value (8) does not specify a valid representation method
Error: example.f90, line 6: KIND value (4) does not specify a valid representation method
Errors in declarations, no further processing for EXAMPLE

Note, however, that integer(2) does signify a supported kind value. We can identify such literal constants also when compiling with the -kind=unique option:

$ nagfor -quiet -kind=unique example.f90 -o example
Error: example.f90, line 4: KIND value (2) does not specify a valid representation method
Error: example.f90, line 5: KIND value (8) does not specify a valid representation method
Error: example.f90, line 6: KIND value (4) does not specify a valid representation method
Errors in declarations, no further processing for EXAMPLE

With -kind=unique, the compiler uses different kind numbers, each larger than 100, to ensure that the values do not match size characteristics. This option also makes the kind values different for each intrinsic kind, to help us detect errors such as that in the following program:

program example
  implicit none

  integer, parameter :: integer_kind=SELECTED_INT_KIND(2)
  integer, parameter :: real_kind=KIND(0d0)

  print '(2(G0.9,:,2X))', 0.1_integer_kind, 0.1_real_kind

end program example

This program uses a literal real constant with kind parameter integer_kind instead of real_kind. This mistake may give the constant a lower precision than desired and will not be detected by the compiler unless the value of integer_kind is not a valid kind for a real entity. Compiling with the -kind=unique option makes the kind number of an integer unusable for a real; using this option we have a compile-time error:

$ nagfor -quiet -kind=unique example.f90 -o example
Error: example.f90, line 7: KIND value (101) does not specify a valid representation method

Note, however, that when using -kind=unique (or -kind=byte to match the numbering scheme more commonly used by compilers), all program units must be compiled with the matching option.

Non-standard intrinsic procedures and non-standard extensions

Code may be written making use of intrinsic procedures or syntax rules offered by the target compiler which are not specified by the standard. Although other compilers may also offer similar extensions, their behaviour may differ. In this way, and to make the code available to users without the particular compiler, it is good practice to avoid these non-standard aspects. For example, the program

program example
  implicit none

  logical :: flag

  flag = 0
  if (1) print '("Something always happens")'
  if (flag) print '("Something was flagged to happen")'

end program example

is accepted by the Intel compiler which supports free conversion between integer and logical types, and accepts integer expressions in conditionals.

The NAG Fortran compiler treats these as errors:

$ nagfor -quiet example.f90 -o example
Error: example.f90, line 6: Left-hand-side of intrinsic assignment statement is of type LOGICAL but right-hand-side is of type INTEGER
Error: example.f90, line 7: Expression in logical IF is not logical

Implicit conversion between integer and logical values may be particularly troublesome with a compiler which uses different rules for the conversion, so it is safer to avoid such non-standard forms.

Finally, the NAG Fortran compiler does not implement such non-standard intrinsic procedures such as the popular iargc and qabs.

Accessing the NAG Fortran compiler

The NAG Fortran compiler is not currently licensed at Queen Mary beyond the School of Economics and Finance. If you are in that school and wish to use the compiler, please contact us. If you are not in that school and do not have your own licence, please contact us to discuss your options for gaining access.

Conclusion

In this post we've looked at using the NAG Fortran compiler to help us develop standard-compliant and portable Fortran code which is free of bugs. Aiming to write portable code means that we have greater freedom to compile and test our programs using multiple tools, as well as being able to more widely share our work.

The NAG Fortran compiler detects many of those problems in our code that may be found by other commonly used compilers. It also has sensitivity to other types of errors and at times clearer diagnostic messages.

Even if we intend to use one compiler on one machine for the "production" version of our program, it's good practice to test with as many tools and machines as possible during its development. We've seen here that the NAG Fortran compiler can help us detect subtle errors which may exist in code. These errors may not be detected with our target compiler but may lead to inaccurate results, or different behaviours when the same code is compiled by another user on a different system.

Acknowledgement

The funding for the licence for the NAG compiler was provided by the fellowship programme of the Software Sustainability Institute. This blog post supports delivery of a training package for researchers developing and maintaining research software.