Fortran skill markdown for codex

The title says it all.

I have started using Codex in VSCode to write Fortran code and is quite useful. However, the code it generates is sometimes inefficient: for example, writes loops not in column-major order (maybe influenced by C and Python?). It writes arrays with old (/ constructor instead of [ etc

So I am looking if there is somewhere some Fortran skill about performance and best practices: Ideally, I want codex to conform to the very good guidelines provided in this website (section on quick start and best practice)

6 Likes

I recently used Claude Opus 4.6 to generate a planetary simulation Fortran code and it wrote a pretty good code, using [ and column-major order. Can you post your prompt that you used that gave you bad code? I can try it and see.

2 Likes

I have never used Codex (or similar tools) yet, so not very sure, but is it possible to ask ChatGPT or other LLMs to generate such a ā€œskillā€ (a kind of task specification?) for generating desired codes by passing the URL above or uploading the PDF version, for example…?

1 Like

I use very little AI, but when I do it is very controlled. I use opencode with zen. TUI only, neovim, git is showing any change instantly on another monitor. My agents.md gets read by the agent prior to every session, and I also have a project synopsis doc I make it read. I specify which subroutines it can use in my libraries, give very, very, VERY specific instructions, and ask it to do nothing that I couldn’t do myself. AI is only used where it costs me less than $3 to have it write code that may take me several hours. The agents.md has all instructions on my preferred style, and fortran standards to use. This includes forcing it to use my equal functions whenever comparing floats, and using [ ] for array construction. I have found that the quality of the code I get is directly related to the quality of my instructions. I have never had this column major issue. I primarily use GPT 5.3 codex. I do use freebie llm’s for clerical stuff like : find every subroutine in this module and make sure they are listed at the top….makes it nice when using ctags in vim, and also good for documentation. I do not use any ā€œskillsā€ nor special ā€œtoolsā€, because well…a year ago, they were worthless, and I don’t see at this point what benefit they have over an agents.md file that gives all basic instructions. The most important thing these days is that agents have finally become more obediant. With a good project synopsis and agents.md I have been able to start each session fresh, I do store compacted transcripts, but I haven’t had to use them. Memory is basically wiped between sessions, and keeping sessions/conversations short, it seems to be working well. I have not had any hallucinations from gpt 5.3. big pickle is dangerous - not obediant, and breaks rules. Good luck!

1 Like

I use this in cursor. It might be useful.

fortran.cursorrules.txt (6.6 KB)

3 Likes

Good list. I have given LLMs similar instructions. I think

Never use print or write statements in pure procedures.

is slightly too broad, because an internal write can be useful in pure procedures and is allowed.

1 Like

I have iterated a bit and created and edited and redited this which I’ve been using. I just added some of the things that were talked about here.

3 Likes

I really liked this list.

A couple of things that I noticed:

  • on implicit save, it might be worth mentioning implicit save inside procedures which is the most typical mistake, specially for beginners, thinking they are doing variable initialization.

  • after having abused of associate at the beginning, I’ve now sobered up because: used in hot kernels, I’ve noticed loss in performance at runtime, it might be an issue with the compilers, but in practice it happens. So now I just use associate on non critical procedures for masking indirection layers when accessing derived types.

  • On stdlib we added this in the style_guide

For module procedures, it is recommended to declare attributes before the module keyword for better retro compatibility (Projects using CMake versions lower than CMake 3.25.0 are concerned see Spurious modules).
Prefer the following pattern:
<attribute> <attribute> module <function/subroutine> <name>
instead of:
module <attribute> <attribute> <function/subroutine> <name>

2 Likes

Could you show an example of how ASSOCIATE adversely affects runtime performance?

1 Like

I’m also curious about this. I rarely use ASSOCIATE because I find the syntax to be very unwieldy when you have a lot of derived type variables you want to locally rename. Would a loss in performance imply that a particular compiler is implementing ASSOCIATE as pointers? Just curious.

1 Like

I have checked this with compiler explorer some time ago, the implementation of ASSOCIATE was exactly the same as the implementation of an explicit pointer pointing to the variable for some compiler and not others.

Disclaimer: I’m not sure if this is the best test, but with this code

subroutine associate_fn(x)
    implicit none
    integer, intent(in) :: x
    integer             :: res
    associate(point => x)
        res = point+point
    end associate
end subroutine associate_fn

subroutine pointer_fn(x)
    implicit none
    integer, target, intent(in) :: x
    integer                     :: res
    integer, pointer :: point
    point => x
    res = point+point
end subroutine pointer_fn

in Compiler Explorer I see that ifx implements it as


while gFortran implements it as:

Given that I don’t know assembly, my interpretation of that output is still that for gfortran associate and explicit pointers produce the same code, while for ifx the assembly code is different (though I cannot decide which one is better)

1 Like

Beside giving an LLM explicit guidelines in a file and compiling with picky options, you can require that all code pass fortitude check with no errors, making that part of the build process. Fortitude is here.

2 Likes

I’ve tried a slightly different code (with res used as the function result):

function associate_fn(x) result(res)
    implicit none
    integer, intent(in) :: x
    integer             :: res
    associate(point => x)
        res = point+point
    end associate
end function

function pointer_fn(x) result(res)
    implicit none
    integer, target, intent(in) :: x
    integer                     :: res
    integer, pointer :: point
    point => x
    res = point+point
end function

Then, the output of CompilerExplorer was something like this:

=== gfortran-15.2 (no option) ===
associate_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -24(%rbp)
        movq    -24(%rbp), %rax
        movq    %rax, -8(%rbp)
        movq    -8(%rbp), %rax
        movl    (%rax), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rbp)
        movl    -12(%rbp), %eax
        popq    %rbp
        ret
pointer_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -24(%rbp)
        movq    -24(%rbp), %rax
        movq    %rax, -8(%rbp)
        movq    -8(%rbp), %rax
        movl    (%rax), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rbp)
        movl    -12(%rbp), %eax
        popq    %rbp
        ret

=== gfortran-15.2 -O2 ===
associate_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        ret
pointer_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        ret

=== ifx-2025.3.2 (no option) ===
associate_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -80(%rbp)
        movq    -80(%rbp), %rcx
        movl    (%rcx), %eax
        addl    (%rcx), %eax
        movl    %eax, -68(%rbp)
        movl    -68(%rbp), %eax
        popq    %rbp
        retq

pointer_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -80(%rbp)
        movq    -80(%rbp), %rax
        movq    %rax, -88(%rbp)
        movq    -88(%rbp), %rax
        movl    (%rax), %eax
        movq    -88(%rbp), %rcx
        addl    (%rcx), %eax
        movl    %eax, -68(%rbp)
        movl    -68(%rbp), %eax
        popq    %rbp
        retq

=== ifx-2025.3.2 -O2 ===
associate_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        retq

pointer_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        retq

=== Lfortran-0.59 (no option) ===
associate_fn:
        movq    %rdi, -8(%rsp)
        movl    (%rdi), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rsp)
        retq

pointer_fn:
        movq    %rdi, -8(%rsp)
        movl    (%rdi), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rsp)
        retq

=== Lfortran-0.59 -O2 ===
associate_fn:
        movq    %rdi, -8(%rsp)
        movl    (%rdi), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rsp)
        retq

pointer_fn:
        movq    %rdi, -8(%rsp)
        movl    (%rdi), %eax
        addl    %eax, %eax
        movl    %eax, -12(%rsp)
        retq

=== flang-20.18 (no option) ===
associate_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    (%rdi), %eax
        addl    %eax, %eax
        movl    %eax, -4(%rbp)
        movl    -4(%rbp), %eax
        popq    %rbp
        retq

pointer_fn_:
        pushq   %rbp
        movq    %rsp, %rbp
        movb    $0, -57(%rbp)
        movb    $1, -58(%rbp)
        movb    $9, -59(%rbp)
        movb    $0, -60(%rbp)
        movl    $20240719, -64(%rbp)
        movq    $4, -72(%rbp)
        movq    $0, -80(%rbp)
        movq    -64(%rbp), %rax
        movq    %rax, -96(%rbp)
        movups  -80(%rbp), %xmm0
        movaps  %xmm0, -112(%rbp)
        movq    %rdi, -48(%rbp)
        movb    $0, -25(%rbp)
        movb    $1, -26(%rbp)
        movb    $9, -27(%rbp)
        movb    $0, -28(%rbp)
        movl    $20240719, -32(%rbp)
        movq    $4, -40(%rbp)
        movq    -48(%rbp), %rax
        movq    %rax, -112(%rbp)
        movq    -40(%rbp), %rax
        movq    %rax, -104(%rbp)
        movq    -32(%rbp), %rax
        movq    %rax, -96(%rbp)
        movq    -112(%rbp), %rax
        movq    %rax, -24(%rbp)
        movq    -104(%rbp), %rax
        movq    %rax, -16(%rbp)
        movq    -96(%rbp), %rax
        movq    %rax, -8(%rbp)
        movq    -24(%rbp), %rax
        movl    (%rax), %eax
        addl    %eax, %eax
        movl    %eax, -52(%rbp)
        movl    -52(%rbp), %eax
        popq    %rbp
        retq

=== flang-20.18 -O2 ===
associate_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        retq

pointer_fn_:
        movl    (%rdi), %eax
        addl    %eax, %eax
        retq

So, if I look only at the number of lines, it seems (for this simple code):

  • With no option, the pointer version often requires more lines;
  • With -O2, both the functions become identical in the assembly output;
  • With -O2, Gfortran, ifx, and flang use only 2 lines for both the functions, while Lfortran uses 4 lines for some reason;
  • With no option, Flang gives a very long assembly code for the pointer version for some reason (but it is reduced to 2 lines with -O2)

EDIT: My main message above is that ā€œthe assembly output changes depending on -O2ā€, rather than ā€œASSOCIATE behaves the same way as raw pointersā€ etc… (I felt interesting to see how compilers can optimize code into a very short one at the assembly level.)

So it appears that the compiler developers took the path of least resistance to implementing ASSOCIATE. Just make the construct arguments the same as pointers. I’ve never seen that mentioned in any documentation for any compiler. Is there a reason that they couldn’t have just come up with some way of parsing the ASSOCIATE construct so that whenever it sees a specified association it just uses the actual argument. ie. treat it as just another name for the associated variable. So why bother with ASSOCIATE at all. To me having a long list of pointer associations is as (probably more) readable than the ASSOCIATE construct. And with the pointers you can always add a CONTIGUOUS attribute in hopes that the compiler will optimize/vectorize the instances where they are used.

When I learned about associate I understood that it was not another way of using pointer sematic, it was an alias for variables and operations. So I expected it to be equivalent to not using pointer but equivalent to just accessing the data directly.

It seems that instead, it was developed in some cases as an alias for pointer sematics. Which is regrettable.

@RonShepard @rwmsu I would love to but can’t because I learned my hard lesson quite a while back in closed source code and I did not take (didn’t have) the time to create a mwe to reproduce and make the runtime loss in performance issue evident.

Actually, my current understanding of ASSOCIATE is that it is very different from the use of Fortran pointers, because (for example in the above code) the point in associate_fn() cannot point to a different thing once it is defined via the ASSOCIATE statement. This is not the case for point in pointer_fn(), in which point can point to a different thing any time after the initial pointer assignment. So, although there is no difference in the assembly code in the above simple case (with -O2), I guess there will appear clear differences in more practical or complicated cases.

We started with that for LFortran but quickly run into problems:

  • For an array expression you don’t want to evaluate it all the time, you want to use a temporary — unless you only access one or a few elements, then you don’t want a temporary. Unless you access the element many times, then you want that element in a temporary.
  • Associate allows you to change the array indexing/strides, so it behaves like a pointer essentially.
  • Some other corner cases that I forgot – I vaguely remember in some corner case we even had to do a copy of the array

So right now we focused on correctness first. Very soon we’ll start looking at performance too, and I think the way to approach it is to try to optimize the simple cases, and it could be that in some corner case it’s just not easy to optimize it out.

There are many places in Fortran where I want to see how far we can push the compiler to optimize things out. I am looking forward to this work, it will be a lot of fun. But first correctness.

In the example of @septc, you can’t do any better than the code that gfortran, ifx, and flang produce (with -O2) for the associate construct. So I don’t understand what the complaint is about how it is implemented.

1 Like

The code snippet provided before is way too simple. It is not representative of actual use cases.

What is being warned about is that the associate construct can prevent maximum optimization in certain circumstances because it might lead the compiler to treat things as pointers instead of aliases.

1 Like

Do you have an example of that?