Debugging and PURE procedure cascade

Printing selected values from an array is often required during debugging (selection criteria are hard to impose in debuggers). But, printing inside PURE procedures is restricted. Being an avid user of small PURE functions and do concurrent, in other words functional style, I have to modify the code unnecessarily just for debugging. Moreover, this leads to a cascade of changes as PURE procedures can only call PURE procedures, and thus I need to modify every other PURE procedure that calls the one under consideration.

There are three ways out that I can think of:

  1. Use preprocessor to change from PURE to non PURE procedures, from DO CONCURRENT to simple DO in debugging mode. But, this will be non standard.

  2. Use error stop cleverly. It cannot cover all use cases, however.

  3. Carry around an extra argument for storing the desired value(s) and print it after you have exited all pure procedures. This will only work for pure subroutines.

Which way, from or other than those listed, is better?

1 Like

In comp.lang.fortran in 2010, in response to the same question, Nick Maclaren wrote

You need to fool the compiler. You can write an external procedure
that just prints an error message and is not pure, and declare it
in a module as PURE. That might work.

If not, you can do the same trick by calling a jiffy little C
function to do the same, though I recommend using standard error
and not standard output. Again, that might work.

Beyond that, there are some system-dependent tricks that bypass
anything that Fortran knows about, but I donā€™t recommend most
people to use them. WTO under MVS, syslog under Unix, and so on.

But there is no guarantee that ANYTHING will work if the compiler
is sufficiently sensitive. Thatā€™s extremely unlikely, but itā€™s
not a good idea to rely on this trick being safe.

Here is an example with an external procedure. The following program,

subroutine thrice(x, y)
real, intent(in)  :: x
real, intent(out) :: y
y = 3*x
print*,"x, y =", x, y 
end subroutine thrice
!
module m
implicit none
interface
pure subroutine thrice(x, y)
real, intent(in)  :: x
real, intent(out) :: y
end subroutine thrice
end interface
contains
pure subroutine thrice_vec(x, y)
real, intent(in)  :: x(:)
real, intent(out) :: y(:)
integer           :: i
do i=1,size(x)
   call thrice(x(i), y(i))
end do
end subroutine thrice_vec
end module m
!
program main
use m
implicit none
real, dimension(2) :: x, y
x = [5.0, 6.0]
call thrice_vec(x, y)
print*,y
end program main

if the subroutine, module, and main program are stored in separate files, compiles and runs with gfortran, giving output

 x, y =   5.00000000       15.0000000    
 x, y =   6.00000000       18.0000000    
   15.0000000       18.000000

Your idea of using a preprocessor was discussed at fortran - Gfortran: Treat pure functions as normal functions for debugging purposes? - Stack Overflow

1 Like

Itā€™s pretty crazy to me that this works honestly. Once again, personally, I really dislike how weak pure is. Iā€™ve never seen a routine that compiled to anything faster because it was pure (or elemental for that matter, in fact elemental is only ever slower, if it measures differently at all), and the supposed promise that ā€œpure routines wonā€™t have side effectsā€ is too easy to get around. Also in this bucket is modifying intent(in) arguments.

I had to check, Fortran 95 introduced user-written pure and elemental procedures. 28 years later and they still seem more like just ideas rather than features that actually add any capability.

Arguably the production version of a code should have all procedures in modules, in which case the compiler wonā€™t be tricked. The procedures called within a do-concurrent block must be pure, and some research has found that do-concurrent can often replace directives. Elemental procedures are convenient, even if they are not faster, obviating the need to write versions of a function with permutations of scalar, 1D and 2D arguments, and the introduction of impure elemental procedures is a nice generalization.

Similarly, argument intents are not guaranteed to be respected if a procedure calls other procedures without intents. but they are still beneficial in expressing the intent of the programmer.

5 Likes

I agree on all points. The unfortunate reality is that for a lot of legacy (i.e. ā€œrealā€) Fortran projects, everything isnā€™t in modules. In that case, things like pure and argument intent sometimes trick a reader of the code into a false sense of understanding.

Honestly I would probably have preferred a compiler error be issued if pure or intent in is specified but the compiler cannot verify it. Just prevent people from keeping around code with incorrect tags. This is similar to comments that describe functionality, but are unmaintained or just incorrect in general.

That would make sense if Fortran 90 were starting from scratch, but when argument intents were introduced in Fortran 90, all the existing Fortran 77 and earlier code did not have intents, so not allowing a procedure with declared intents to call such code would have greatly reduced the applicability of argument intents.

Well, no. They can be used in modules. New feature only works in the new style, thatā€™s the rule.

Also, it could still call them, but if my subroutine has argument a marked intent(in), then I canā€™t supply a to another procedure down the call stack as anything other than verifiable intent(in). I could still have local variables with no intents that get passed around like normal.

This is only true when calling procedures without explicit interfaces. The compiler just assumes you know what youā€™re doing if youā€™re calling procedures with implicit interfaces (because thatā€™s what is backwards compatible). But, if you want to be warned about such things then add

implicit none (type, external)

Iā€™m in favor of @Beliavsky approach to solving the ā€œprint in a pure procedureā€ problem, but ONLY FOR DEBUGGING. NEVER USE THE TRICK IN REAL CODE. Once you turn on optimizations, multi-threading, etc., youā€™re likely to hit race conditions in run-time and system libraries that make output inconsistent, and possibly even cause corruption.

1 Like

How about using a block statement? Here is a modified example by @Beliavsky.

module m
   implicit none
contains
   pure subroutine thrice_vec(x, y)
      real, intent(in)  :: x(:)
      real, intent(out) :: y(:)
      integer           :: i
      do concurrent (i=1: size(x))
         y(i) = 3*x(i)
         DEBUG: block
            print*,"x, y =", x(i), y(i)
         end block DEBUG
      end do
   end subroutine thrice_vec
end module m
!
program main
   use m
   implicit none
   real, dimension(2) :: x, y
   x = [5.0, 6.0]
   call thrice_vec(x, y)
   print*,y
end program main

I donā€™t think that a block statement removes the purity guarantee. I tried your code and GFortran gave me an error:

block.f90:12:39:

   12 |             print*,"x, y =", x(i), y(i)
      |                                       1
Error: PRINT statement at (1) not allowed within PURE procedure

Yes gfortran gives error. ifort and ifx execute without any issues and nvfortran shows a warning but still displays the results:

ifort (IFORT) 2021.11.0 20231010
Copyright (C) 1985-2023 Intel Corporation.  All rights reserved.

 x, y =   5.000000       15.00000    
 x, y =   6.000000       18.00000    
   15.00000       18.00000
ifx (IFX) 2024.0.0 20231017
Copyright (C) 1985-2023 Intel Corporation. All rights reserved.

 x, y =   5.000000       15.00000    
 x, y =   6.000000       18.00000    
   15.00000       18.00000
nvfortran 23.7-0 64-bit target on x86-64 Linux -tp haswell 
NVIDIA Compilers and Tools
Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES.  All rights reserved.
NVFORTRAN-W-0155-thrice_vec - PURE subprograms may not contain external I/O statements (deb.f90: 11)
  0 inform,   1 warnings,   0 severes, 0 fatal for thrice_vec
 x, y =    5.000000        15.00000    
 x, y =    6.000000        18.00000    
    15.00000        18.00000 

Putting the legacy code as is in a module is not enough here, you would also have to add intents to all dummy arguments.

My mistake - the original message was unclear: Argument intent could be used in modules. As the ā€œnew featureā€ being discussed, they would only be usable/applicable alongside the other new feature, modules. Yes, simply copy/paste old code in would be insufficient, and that is ok.

Even in modern programming there are many situations where the developer makes promises that the compilers cannot verify. For instance when writing a parallel loop (with do concurrent or OpenMP), the developer is essentially saying that there is no dependency between the iterations, and in the general case the compiler cannot know if itā€™s correct or not.

1 Like

:scream:

But who can guarantee that a future version of ifx wonā€™t bother?

I tried fpp, but putting ā€˜ā€˜pure &ā€™ā€™ everywhere looks ugly!

Why canā€™t we have a dedicated debugging unit enabled only with -g compiler option?

:thinking:

And a statement in the standard along the lines of ā€œOutput to the preconnected file identified by DEBUGGING_UNIT is allowed in pure and simple procedures, but it is processor dependent whether such output is externally observableā€ with probably some wordsmithing to say it just right. Now Iā€™m sure thereā€™s nuances and gotchas here Iā€™m not thinking about, but it seems like an idea worth considering.

1 Like

The thread Attribute for ā€œpureā€ procedures that do I/O was about debugging, and in it @plevold mentioned the project GitHub - plevold/fortran-debug-utils: Some debug utilities for Fortran

I think this is a good idea in general. ERROR_UNIT is already defined in the standard, and most shells already have ways to direct and redirect this output to various places (screen, named file, pipe, combined with standard output, etc.), so why not just use that unit number with its established functionality instead of introducing a new one?

1 Like

Inspired by this project, I developed ForDebug. This tool allows printing or writing variables of different types and kinds to the terminal or a file. Additionally, it is capable of measuring elapsed time in pure procedures.

1 Like