Intel Fortran doesn't enforce argument kind conformance?

I never noticed that until today, but apparently the following code compiles with any ifort/ifx version, just with a warning, and gives garbage:

program foo
implicit none

integer(8) :: i = huge(0) + 1_8

print*, "i = ", i
call init(i)

contains

    subroutine init(a)
    integer, intent(in) :: a
    print*, "a = ", a
    end subroutine init

end

Ouput with ifx 2024:

/app/example.f90(7): warning #6075: The data type of the actual argument does not match the definition.   [I]
call init(i)
----------^

Program returned: 0
Program stdout

 i =             2147483648
 a =  -2147483648

That’s crazy :frowning: …

Second surprise (at least with my version of ifx/ifort )was that I was going to say a nice feature of the Intel compilers is that you have a lot of control over what happens for each diagnostic id. So you can do this


ifort -diag-disable=10448 -diag-error=6075 main.f90

and you quit getting reminded ifort is obsolete, and message 6075 is now an error. You can even set up a system-wide default or a user-level default and change that. The surprise was when I used ifx I did not see an error indicating ifx did not support that, but it is still a warning.

ifx -diag-error=6075 main.f90

So I have had a customization file with changes to include my own libraries in the default load, to default to a static load, and so on for so long that I did not notice it was not working with ifx.

That really is a crazy default for procedures in modules and contained procedures but there is a lot of legacy code around that expects to be able to use an array as any type of working storage, and to allow equivalencing of different types and scalar values passed as one-element arrays and vice-versa that it is probably the default because there does not seem to be any control of whether 6075 applies to things only without explicit interfaces or to everything.

Almost every compiler allowed that with no warning so commonly it became a de-facto feature of Fortran, although the first time it is mentioned in the standard it says the arguments have to match to be standard conforming. That is the most vexing issue over and over again when I have ported older applications to standard-conforming coding, as that is not always trivial to resolve. (sometimes you just pass A as [A] if the procedure does not change it, or A(n) as A(n:n) and maybe make two working arrays, one integer and one real, … so sometimes it is easy). So this Intel “feature” lets you put function statements that broke the rules in a contained procedure, and regular procedures into a module more easily because of this behavior, but then the code does not work with a lot of other compilers.

I was going to suggest fpm should probably add some -diag-IDs to -diag-error but if ifx ignores this that is not too helpful. If anyone has the latest ifx can they confirm it does not change from a warning to an error on the latest-and-greatest?

PS: The first machine I used that did a nice job with diagnostic codes that I remember was a Cray. You could turn the messages on and off by number, and a program called “explain” told you what to do about the message in greater detail if it was not obvious. Nothing else I was using allowed as much control for a long time, and I always missed the feature when not available. Later g95 and Intel compilers allowed some control, and the -diag-* option on ifort is quite a powerful customization option. I hope all the up and coming compilers allow that. I do not currently have access to a Cray, but the explain command was multi-lingual and you could create your own message catalog for use with the command. Very nice. It had/has an API too so you could use that with your own codes as well. We still follow that model with production codes; just the act of writing the documentation for something like “explain” can make you realize the code needs changed – generally if it is really hard to explain what the message means you should think about whether the code should be doing something else!

Here is the output from a few other compilers (including flang and nvfortran): Compiler Explorer

flang:

error: Semantic errors in /app/example.f90
/app/example.f90:7:11: error: Actual argument type 'INTEGER(8)' is not compatible with dummy argument type 'INTEGER(4)'
  call init(i)

nvfortran 24.9:

NVFORTRAN-S-0450-Argument number 1 to init: kind mismatch (/app/example.f90: 7)
  0 inform,   0 warnings,   1 severes, 0 fatal for foo
Compiler returned: 2

This permissivity is still possible with ifort/ifx with the -nogen-interfaces compilation option. At least the default behavior of ifx (without any compilation option) on this point is standard conforming. The problem with the code snippet I have posted is that the default behavior is not standard conforming (BTW is there an ifort/ifx option to enforce strict standard conformance?)

I thought -standard-semantics does it, but it doesn’t seem to enforce it. :hushed:

The question is whether the type mismatch is a named constraint (and how precisely it is worded by the standard) that the compiler is obliged to analyze/report. A starting point to search would be 15.5 in J3/24-007. For instance in section 15.5.2.5 (not constraints):

The dummy argument shall be type compatible with the actual argument.

The kind type parameter values of the actual argument shall agree with the corresponding ones of the dummy argument.

On the other-hand even named constraints don’t really say whether violations should be errors or warnings (section 4.1.2), just the processor should have the ability to diagnose them:

… a standard-conforming processor is required only to have the capability of diagnosing violations of the constraint.

1 Like

OK. But at which point did someone decide that he would be a good idea to authorize such a violation? The Intel compiler intentionnaly casts the integer(8) into an integer(4) here (note that the compiler reports an error if the dummy argument is intent([in]out)).

EDIT: actually, authorizing in the standard the implicit casting of the actual argument (based on the same rules as in expressions) when the dummy argument is intent(in) could be a good idea. The problem here is that 1) it’s completely unexpected (as it’s not in the standard) and 2) it’s not consistent (the compiler won’t cast between a real and an integer, or between a real(4) and a real(8)… it works only between different integer kinds)

From the Intel Fortran Reference Manual on Build Process Errors:

NOTE:

If you have a question about a compile-time error, submit your question to the IntelÂŽ Fortran forum.

Another option you could use is to raise all warnings to errors: -warn all,errors.

Edit: just saw your edit. I agree - the casting between integers is a suspicious default.

Casting everything in K&R C did not gain a lot of fans in the long run, but for procedures not called a lot I use class(*) and functions that cast any intrinsic to a real or double or large integer so I do not have to duplicate code over and over to create a generic; particularly if I want more than one parameter to be of various kinds so having something on intent(in) variables like a “castable” option sounds appealing. Templating will hopefully help address that as well but allowing intent(in,castable) sounds appealing, particularly if it was restricted to only casting to a larger kind.

subroutine castit(a,b,c,answer)
real(kind=real64),intent(cast)  :: a, b, c
real(kind=real64) :: answer
answer=a*b*c
end subroutine castit

where a,b, and c could be anything they could have been if the routine call was replaced with “answer=a*b*c” is appealing at first glance, at least.

So even with the latest ifx version does -diag-error=6075 still not produce an error? So far I have not found documentation indicating it should not work with ifx, works great with ifort.

So so far no other compiler allows the original post to compile, at least by default. So it is a very non-portable extension at best to allow it.

If anyone wants to play with the idea using currently standard methods see

The README shows an example procedure that can be called with many of the most common numeric types and kinds with some of the same issues like demotion without having to call out_of_range (which could be added). So it has its caveats but a lot simpler than other methods, particularly when each argument might be different types.

1 Like

I’ve never understood the reasoning behind CLASS(*) for passing intrinsic types. Why can’t we just have INTEGER(*) or REAL(*) work for all cases not just C-Interop. and save CLASS(*) for things that match the classic definition of a class (ie a derived type or structure that defines both data and methods that act on that data). To me something like INTEGER(*) has a big advantage because it can be restricted to just integers. In my experience, the case of using an integer or character string to pass any kind of data was used a lot in the old days to pass data to a C routine (mimicing a void pointer). The current Fortran C interop facility makes that old programming trick unnecessary. Removing the restrictions on assumed type variables would provide another way to implement generic procedures without all of the boilerplate that comes with templates etc.

1 Like

It could be class(integer), class(real), class(numeric)…

Jane and I remember raising an issue with Intel (a long time ago) about the following example.

program ch0504
implicit none
real :: light_minute, distance, elapse
integer :: minute, second
real, parameter :: light_year = 9.4610**12
! Light_year : Distance travelled by light
! in one year in km
! Light_minute : Distance travelled by light
! in one minute in km
! Distance : Distance from sun to earth in
! km
! Elapse : Time taken to travel a
! distance (Distance) in minutes
! Minute : integer number part of elapse
! Second : integer number of seconds
! equivalent to fractional
! part of elapse
!
light_minute = light_year/(365.25
24.060.0)
distance = 150.0
10**6
elapse = distance/light_minute
minute = elapse
second = (elapse-minute)*60
print *, ’ Light takes ‘, minute, ’ Minutes’
print *, ’ ‘, second, ’ Seconds’
print *, ’ To reach the earth from the sun’
end program

The Intel compilers compile and run the program and generate the correct physical result.

Light takes 8 Minutes
20 Seconds
To reach the earth from the sun

They ‘correct’ the maths in the the following statement

real, parameter :: light_year = 9.46*10**12

The following 10**12 should cause integer overflow.

The reply was one customer ‘paid’ for this behaviour :frowning:

This is a common practice in assembler, especially on little-endian machines. If the function only needs to use, say the low-order 4 bits in an argument, then it doesn’t matter if the argument is INT8, INT16, INT32, INT64, or INT128. There is no “casting” (i.e. conversion) involved, it just looks at the bits that it needs to do its job and ignores the rest, and the programmer needs only to write one function instead of two, or three, or four, or five functions to handle all possible argument kinds. So it is natural to think this could be done also in a high-level language, at least with enough compiler options available to turn off the TKR matching that should occur normally.

As for whether this nonstandard behavior is the default is still an ongoing matter of debate. Older compilers have an installed base of legacy behavior that they must support to satisfy their existing users and customers. But even newer compilers, such as lfortran, have chosen to adopt nonstandard behaviors as the default (e.g. default IMPLICIT NONE, and so on). This choice means that such compilers will not correctly compile standard conforming code. The language standard cannot, in a practical way, enforce when a compiler produces object code (i.e. whether a violation is a warning or an error), which extensions are or are not allowed, and even what the compiler defaults should be. The language standard can really only say what a conforming compiler should do with conforming code. The other three combinations are not controlled, or at least not fully controlled, by the standard.

1 Like

I checked, and in the above case there is actual casting.

1 Like

Attention @greenrongreen , @devorah

It is a bug I think that
ifx -diag-error=6075 main.f90
does not change the warning to an error level. I see the driver passing off
-diag-enable-disableE:6075
to xfortcom which SHOULD change 6075 to an error level. It’s odd in that it should be the same code path as ifort. I will get a bug report going on this and alert the team to this tomorrow.

2 Likes

In case it is useful, -diag-dump works with ifort, not with ifx as well, and ifx -diag-error=all changes 6075 (and everything else, as intended, as far as I can tell) to an error successfully; and when I queried the tag for 6075 from ifort and tried to use it instead of the error number that worked with ifort but caused the ifx command to fail saying the name was unrecognized. The one I really care about myself is -diag-error=6075 working; but it would be nice if it all worked as with ifort where it has been a powerful and useful option.

using -diag to ignore 6075 works, perhaps surprisingly.

the bug ID for diag-error=6075 is CMPLRLLVM-62642
We looked at this in our team meeting today. Like me, the team was very surprised by this one. We THOUGHT the code path was shared with ifort, apparently not. One of our front-end people is digging in to find and fix. We’re wondering about other cases of specific error numbers and will make sure we do a thorough code review (make sure we don’t just fix for 6075 but for any message ID).

3 Likes

Not quite as dramatic, but every compiler seems to allow things like

a=max(10.0,5.111111111d0,30.0d0) 

and not report it. Most compilers promote it to the same kind as if an
expression like a+b+c was on the RHS when things like min(a,b,c) are used, but one compiler always returned the kind of the first argument, which led to a subtle bug where digits of precision were lost. Essentially the program was similar to the common issue of a=1.111111111111111111111 (assuming A is doubleprecision) being used instead of a=1.11111111111111111d0. It was a while ago, but I just tried two compilers and did not get warnings about the kinds not being the same. Maybe some older standard versions did not require the kind to be the same for all arguments for some intrinsics? There might be some options that make it produce a warning but if so, have not found them yet.

The max intrinsic has other issues too, two being signed zeros and optional arguments. More details found here: GitHub - klausler/fortran-wringer-tests: A collection of non-portable Fortran usage, standard-conformant or otherwise

In the latest interpretation doc (J3/24-007) it says for max(a1,a2 [, a3, ...]) (16.9.135):

The arguments shall all have the same type which shall be integer, real, or character and they shall all have the same kind type parameter.

1 Like

The wringer tests is a good read. Thought I had hit most such issues but definitely some new ones there. Once again it makes me wish there was a set of test programs supported by the standards committee that all the compilers were tested against. In lieu of that the tests along with the descriptions are a great resource. “Follow the well-traveled path” may not be as fun as using the latest and greatest features or poking into the dusty corners but this list shows why that old quote can be good advice!