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.