Proposal: IMPLICIT NONE (INTENT)

Fortran 2018 introduced IMPLICIT NONE (EXTERNAL), which induces me to hope that compilers will soon enable external procedure calls to be checked that the procedure actual arguments as well as return type (if the procedure is a function) are consistent with the declarations of the types of the function and its arguments.

In this note I wish to make the case for IMPLICIT NONE (INTENT). Intent declarations are not required, and the default intent is INTENT(IN OUT). When a programmer writes explicit INTENT declarations instead of relying on the default, the programmer would appreciate help from the compiler (when requested with an option flag) to check that the declared intents are correct. In the case of the NRL/NASA code HWM14, about which I opened a separate thread a few weeks ago, there are some instances where the declared intent is wrong, and there is no way that I can see to get the compilers to detect and tell me about that error The opinion has been stated that compilers usually ignore intent declarations completely. In the case of HWM14, I was able to pinpoint the errors in the stated intents only by turning on the C=UNDEFINED option of the NAG compiler. To understand the issues better, I went back to the HWM14 source files, and removed all the intent declarations (the correct ones as well as the incorrect ones). The result, when compiled with or without optimization, was: surprise, no change in the output results. Are we to conclude that writing intent declarations, without help available for checking those declarations, is a waste of time?

2 Likes

I think the default intent should be intent(in), so that you don’t accidentally modify the arguments and because I think that’s the most common.

3 Likes

My experience, admittedly only with the DEC/Intel compilers. is that INTENT is checked for violations of language rules. Generated code changes are more subtle, and only with INTENT(IN).

2 Likes

The default is no INTENT specified, which is not the same as INTENT(INOUT). INTENT(INOUT) specifies the argument is definable, so technically you could not pass an expression, for example.

INTENT(OUT) means I am going to specify the value on return but do not need the current value, INTENT(INOUT) means I need the current value and am going to set the value on return and no INTENT means no such assumptions. So both
INTENT(OUT) and INTENT(INOUT) imply the argument passed is definable and should not be a constant or expression, for example.

You might get the same answer if you remove all the INTENT attributes and they were done correctly but perhaps not the same optimizations. Passing an argument that the compiler decides to do a gather/scatter on would not need to do the scatter if INTENT(IN) is specified, for example. The standard leaves nearly everthing up to the compiler but nothing prevents the compiler from enforcing INTENT, it is just very difficult to enforce.

NOTE 4 in section 8.5.10 gives perhaps the best condensed description; although the behavior with pointer arguments is a bit harder to digest.

Obviously at least one compiler actually uses the INTENT(OUT) description as it caused the bug you described; so it is not like INTENT always has no effect; but the machine code produced is often the same whether the option is there or not; partly because the compiler can be smart enough to draw the same conclusion; perhaps because the compiler treats it merely as a comment mostly useful to someone maintaining the code in the future.

Because of the very bug you describe that can be introduced I have seen several discussions where people say they remove all the INTENT attributes, but I find it useful myself.

 8.5.10      INTENT attribute
          NOTE4
           Argument intent specifications serve several purposes in addition to documenting the intended use of dummy
           arguments. A processor can check whether an INTENT (IN) dummy argument is used in a way that could
           redefine it. A slightly more sophisticated processor could check to see whether an INTENT (OUT) dummy
           argument could possibly be referenced before it is defined. If the procedure’s interface is explicit, the processor
           can also verify that actual arguments corresponding to INTENT (OUT) or INTENT (INOUT) dummy argu-
           ments are definable. A more sophisticated processor could use this information to optimize the translation of
           the referencing scoping unit by taking advantage of the fact that actual arguments corresponding to INTENT
           (IN) dummy arguments will not be changed and that any prior value of an actual argument corresponding to
           an INTENT (OUT) dummy argument will not be referenced and could thus be discarded.
           INTENT (OUT) means that the value of the argument after invoking the procedure is entirely the result of
           executing that procedure. If an argument might not be redefined and it is desired to have the argument retain its
           value in that case, INTENT (OUT) cannot be used because it would cause the argument to become undefined;
           however, INTENT (INOUT) can be used, even if there is no explicit reference to the value of the dummy
           argument.

           INTENT (INOUT) is not equivalent to omitting the INTENT attribute. The actual argument corresponding
           to an INTENT (INOUT) dummy argument is always required to be definable, while an actual argument
           corresponding to a dummy argument without an INTENT attribute need be definable only if the dummy
           argument is actually redefined.

I wish it was enforced more often, and I wish it was implemented on the procedure header itself and optionally on the call as well, so it looked like

subroutine(a>,b>,<c>,<c) or (a>,b>,c<>,d<). If that notation meant a call had to use matching notation it would be easier for a compiler to enforce, but that would mean if you changed the routine you would have to change all the calls and is perhaps too burdensum a syntax.

I think the default being anything but the current lack of intent would be problematic for a subroutine, but I do wish functions defaulted to everything being INTENT(IN) as well, but that is somewhat a personal taste I suppose ( I really like to avoid changing input values with a function, but admit to doing it occasionally with an optional error code parameter).

4 Likes

That sentence and the one following it in your quotation are very much relevant and helpful.
The original programmers of the HWM14 code, I guess, added intent declarations in order to be helpful to people who used their code years later. Despite their good intentions, a bug was introduced into the code – a bug that has survived for12 years.

I hit a similar bug a while ago which is where I first learned how IINTENT(OUT) could optionally undefine the current values. One compiler used a new undefined array at the beginning of the procedure, but the procedure did not always set all the values but another compiler treated it just like no intent was specified (and worked) . It was a Netlib package but I do not immediately remember which one. It is unfortunate the attribute can cause new bugs and does not always catch old ones; making its use more problematic than it originally appears. I know when I initially saw it I thought it would always be an improvement to use it but found out in practice that is not the case.

With large arrays there can be significant overhead in initializing an array, but if I suspect intent is causing a problem I remove them all or for all INTENT(OUT) arrays put ā€œARRAY_NAME=NANā€ for floats and ARRAY_NAME=(-huge(0)) for whole numbers at the very top of the procedures.

Not sure if it is a common enough problem to justify a compiler having a ā€œā€“ignore intentā€ switch, but there are times it would be nice.

It is one of those classes of bugs where once you see and understand it you remember because it took so much effort to resolve, and subsequently spot it pretty quickly. But definitely a lesson I wish was not needed.

2 Likes

More than that, if the intent(out) argument is a derived type with initializers, they will be initialized:

program intent_out_test
  implicit none

  type xyzzy_t
    integer :: i=42, j=-42
  end type

  type (xyzzy_t) :: x

  x%i = 10
  x%j = 11
  call sub (x)
  print *, 'main: ', x%i, x%j

contains

  subroutine sub (t)
    type (xyzzy_t), intent(out) :: t

    print *, 'sub : ', t%i, t%j

  end subroutine

end program
$ lfortran intent_out.f90
sub :     42    -42
main:     42    -42
$

Yes. Older codes tend not to use user-defined types but a common thing in the old codes is passing the same parameter multiple times. The two combined is indeed a perplexing combination. I was not sure at all what
a modified version of your demonstrator would produce or if it would compile…

program intent_out_test
  implicit none

  type xyzzy_t
    integer :: i=42, j=-42
  end type

  type (xyzzy_t) :: x

  x%i = 10
  x%j = 11
  print *, 'main: before', x%i, x%j
  call sub (x,x,x)
  print *, 'main: after', x%i, x%j
 
contains

  subroutine sub (s,t,u)
    type (xyzzy_t), intent(in) :: s
    type (xyzzy_t), intent(out) :: t
    type (xyzzy_t), intent(inout) :: u

    print *, 'sub : ', t%i, t%j
    print *, 'sub : ', u%i, u%j
    t%i=t%i+10
    u%j=u%j+100

  end subroutine

end program

so the result was a little anti-climatic given my pessimism! Only tried it
with gfortran though.

Others have pointed out that the default intent is not exactly the same as INOUT. Consider this program:

program intent
   implicit none
   integer :: a, b, c
   call sub( 1, 2, 3 )  ! error in third argument
contains
   subroutine sub( a, b, c )
      integer :: a   ! no intent
      integer, intent(in) :: b
      integer, intent(inout) :: c
      b = b + 1  ! should be an error.
   end subroutine sub
end program intent

$ gfortran intent.f90
intent.f90:10:7:

   10 |       b = b + 1  ! should be an error.
      |       1~
Error: Dummy argument 'b' with INTENT(IN) in variable definition context (assignment) at (1)
intent.f90:4:19:

    4 |    call sub( 1, 2, 3 )  ! error in third argument
      |                   1
Error: Non-variable expression in variable definition context (actual argument to INTENT = OUT/INOUT) at (1)

It is allowed to pass the literal value 1 as the first argument with default intent, but not the literal value 3 as the third argument with INTENT(INOUT).

Also note that the modification of the second dummy argument with INTENT(IN) is recognized by the compiler as an error.

Changing the meaning of default INTENT would be problematic because of the 70 year legacy of fortran arguments. It would have been problematic if it had been changed in f90 too (when INTENT was introduced into the language), but perhaps even more so now some 35 years later.

There are some other cases where intent(out) requires some kind of active action by the compiler: 1) an allocatable actual argument (or derived type component) must be deallocated, and 2) intent(out) can trigger a finalizer call. Are there any others?

1 Like

I’d prefer a default intent(in). Subroutines will often have arguments that should be declared intent(out) or intent(in out), so the point of default intent(in) would be to force the programmer to supply proper intents.

2 Likes

The allowed values for the intent() declaration are in, out, and inout. If the default for subprogram arguments were changed to in, then how would a programmer specify the current default intent? And how would such a change in the default intent be implemented in the millions of lines of legacy code that depend on the current default?

Any time programmers are forced to do something different than what they prefer, there will be resistance.

They would only be forced to change code when a line such as

IMPLICIT NONE (INTENT (IN))

appears in the program unit. I should not have said that this would be the default.

4 Likes

I think that the introduction of INTENT was a positive step, but more could be done:

fpt is an analysis and re-engineering tool. The intents which it recognises are:

  • INTENT NONE The argument is not used. This is not as silly as it sounds - there may be a suite of routines which all have the same arguments.
  • INTENT ATTRIB READ The data in the argument is not used. Its attributes are read, e.g. bounds, allocation status … (this could be expanded)
  • INTENT ATTRIB WRITE The attributes may be changed - e.g. by allocation, pointer operations etc.
  • INTENT READ ONLY The data May be read but are not modified
  • INTENT READ The data May be read
  • INTENT WRITE The data MAY be written and MAY be passed through unchanged.
  • INTENT WRITE ALWAYS The data is not read before it is written and is ALWAYS changed
  • INTENT SUBROUTINE The argument is a subroutine
  • INTENT FUNCTION The argument is a function
  • INTENT LABEL The argument is a statement label (Yes yes I know, but compilers still accept it)

The point of all this is to establish variable lives, so as to follow things like units and dimensions, and to find whether a variable could be uninitialised. The categories are not always mutually exclusive. For example, the allocation status and the value of a variable can both be changed in a routine. Data may both be read and written - INTENT READ+WRITE

So I would like to see the idea of INTENT extended.

For the current intents defined in the language we have found:

In our large library of codes, about 2% of INTENT(IN) declarations are incorrect. The variables can be written to. Compilers cannot check if they cannot see the interfaces.
The proportion of INTENT(OUT) violations is smaller but still significant. The variables are read before they are assigned.

We have an fpt command to strip all non-mandatory INTENT declarations. We have run this on WRF, and on a number of other codes. The results have never changed.

But we have found ONE case where INTENT(OUT) and INTENT(IN) actually change compiler behaviour. If an array is passed as a non-contiguous array specification
e.g. CALL foo(a(1:100:2))
the compiler is able to choose whether the array is imported and exported depending on the intent. This shows in the time taken to make the calls.

WRT the proposal, what INTENT would be applied to a function or subroutine formal argument? I think that to use this we would need to expand the INTENT categories.

6 Likes

While I understand what would mean implicit none (intent) (that every dummy arg must have an intent declaration), I am confused by @Beliavsky’s proposal. If it were to mean ā€œthe default intent is (in)ā€, I would rather expect something like implicit (intent(in))

For the time being, for anyone interested in enforcing the usage of intent, you can use the linter Fortitude.

2 Likes

Strongly recommend Fortitude. But e.g.

PROGRAM t_ext
        IMPLICIT NONE
        INTEGER,EXTERNAL :: fun1
        INTEGER,EXTERNAL :: fun2
        CALL caller(fun1)
        CALL caller(fun2)
END PROGRAM t_ext ! ************************

INTEGER FUNCTION fun1(i)
        IMPLICIT NONE
        INTEGER,INTENT(IN) :: i
        WRITE(*,'("In fun1")')
        fun1 = i
END FUNCTION fun1 ! ***********************

INTEGER FUNCTION fun2(i)
        IMPLICIT NONE
        INTEGER,INTENT(IN) :: i
        WRITE(*,'("In fun2")')
        fun2 = i
END FUNCTION fun2 ! ***********************

SUBROUTINE caller(f)
        INTEGER,EXTERNAL :: f
        INTEGER :: i
        i = f(42)
        WRITE(*,*) i
END SUBROUTINE caller ! ********************

Fortitude flags:



t_ext.f90:27:29: C061 subroutine argument 'f' missing 'intent' attribute
   |
26 | SUBROUTINE caller(f)
27 |         INTEGER,EXTERNAL :: f
   |                             ^ C061
28 |         INTEGER :: i
29 |         i = f(42)
   |

What INTENT would we like for f ?

This was fortitude 0.7.3 - it may not happen in the latest version.

1 Like

It’s because you should be using explicit procedure :sweat_smile: .
I also noticed another edge case with uddtio:

subroutine stats_write(dtv, unit, iotype, v_list, iostat, iomsg)

        class(statistics), intent(in)   :: dtv

        integer, intent(in)             :: unit

        character(*), intent(in)        :: iotype

        integer, intent(in)             :: v_list(:)

        integer, intent(out)            :: iostat

        character(*), intent(inout)     :: iomsg
!...
end subroutine

I am getting an error C072:

C072 character ā€˜iomsg’ has assumed size but does not have intent(in)
   |
75 |         integer, intent(in)             :: v_list(
76 |         integer, intent(out)            :: iostat
77 |         character(*), intent(inout)     :: iomsg
   |                   ^ C072
78 |         !private
79 |         real(rp) :: dt
   |

But I cannot change the signature here so I simply ended up disabling the error. You can do it in the fpm.toml file directly, for instance:

[extra.fortitude.check]
select = ["C", "E", "S"]
ignore = ["S001", "S061", "C121", "C003", "C132", "E000"]
line-length = 132

Yes, that would be better syntax.

Thanks. I downloaded Fortitude on Windows, and the download went smoothly. One command and the EXE is installed in users’..'.local.bin. I am going to give it a good workout, since it is easy to use. However, on the main issue of this thread, it failed. Here is short test code:

subroutine dwm07(DW)

    implicit none

    REAL,intent(in)      :: AP(2)
    REAL,intent(out)     :: DW(2) !wrong intent, should be in Out

    real           :: pe, pn, qe, qn
  
    pe=2.1; pn=2.2; qe=2.4; qn=2.5

    dw(1) = qn*1.0 + pn*2.1
    dw(2) = qe*1.3 + pe*3.1

    !APPLY HEIGHT PROFILE
    dw = dw / (1 + exp(-(alt - talt)/twidth)) ! Bug: dw is undefined since its intent is 'out'

    return

end subroutine dwm07

! See https://github.com/jacobwilliams/HWM14/blob/master/src/hwm14.f90. Line 752

@mecej4, indeed, fortitude will only catch if there are missing intents. When the wrong intent is specified, then only your compiler should catch the issue (at least if you specify intent(in) and tend to modify the variable).

At the moment I did not find anything better than specifying all the intents as `intent(in)`, let the compiler tell me when this is wrong and correct manually the code based on what I see it should do.

1 Like