What are the versioning/dev rules of stdlib?

I recently downloaded the stdlib sources from the github repo, and was surprised to see that it was still marked as v0.7.0 (released 07/2024), although the files have been regularly updated since then (to be clear, I did not download the 0.7.0 release, but the files from the repo as it was at the moment). This makes me wonder how the versioning is managed?

This page stdlib/WORKFLOW.md at master · fortran-lang/stdlib · GitHub says that the new features are under “experimental”, but I’m not sure what it means, as I can’t see neither “experimental” (or related) branches nor some kind of “stdlib-experimental” repo… Maybe they are not public?

So, what are the changes in the main repo since the v0.7.0 release?

  • are they only corrective maintenance, code polishing, etc…?
  • if yes, does the absence of any v0.7.x (x>0) mean that no bug has been found and corrected since the release of v0.7.0 ?
  • Is there a risk of backward compatibility break (I mean, not unwanted) between the sources downloaded at an arbitrary time T1 another time T2>T1 ?
  • How can I know what I have downloaded, apart from keeping track of the date of the download ?
1 Like

The “experimental” designation is what we started with 5 years ago, so that we can get started and not bogged down with discussions about what exact API the standard stdlib should have.

Now with 5 years of experience I think it’s time to promote some of these “experimental” parts into “release”.

It would be good to clarify what is still experimental and what is stable.

CC @hkvzjal, @FedericoPerini, @jeremie.vandenplas.

1 Like

You are right to signal these points @PierU!

The exact image of version 0.7.0 is found here Releases · fortran-lang/stdlib · GitHub. Since that point, the main branch has evolved, I would say a lot, not only bugs have been fixed but also major features have been introduced. To my knowledge there is one breaking change regarding the get_line function which was previously named getline (switch to snake_case naming convention)

When you download code directly from the main branch in the github repo, it corresponds to the most up-to-date version.

The official announcement for the new release should come soon.

@certik and if there are other members who participated at the creation of stdlib at the beginning, I wonder if the 5th point on the workflow should be revisited:

  1. Release: Moving from experimental to release. The experimental “namespace” contains new functionality together with its specification. In order to move from experimental to release, the specification document must be approved by the wide community and the standards committee (informally). If that happens, it has now been blessed for broad use and we can move the code into the main section of stdlib, and the particular specification document becomes part of the Fortran Standard Library.

When I read this, it sounds pretty daunting, specifically the “… and the standards committee (informally). If that happens, it has now been blessed for broad use” part. I reckon that it does makes sense from a theoretical point of view. Practically speaking though, it might not be that easy to put into practice. Reading this I understand that we need one (or several) person(s) from the standards committee to watch over stdlib and put a seal of approval (even if informally, whatever this means).

A solution I see to solve this stalemate is to amend this article 5. Something like “a feature which has not required major fixes, nor API signature modifications between 3 releases can be tagged as a stable feature”. It is not bullet-proof, it can also mean that nobody paid attention to it so it didn’t require to be changed … no news, good news ?

2 Likes

To add to what @certik and @hkvzjal already correctly said, the upcoming release (I bet it will be 0.8.0) will contain two major new features:

  • the sub-processing module
  • an initial system module
    along with several bugfixes and build system improvements.

Not necessarily, it is a heuristic process. Releasing a new version can be tedious, so the practice so far has been to wait until meaningful updates are coalesced around it, and @jeremie.vandenplas has done a great job at it. But I agree with you that with the recent acceleration in stdlib features, maybe we should release versions more often :slight_smile:

Technically, all procedures marked “experimental” may be subject to changes in the API. But many have been so for a long time. I bet because the original aims of the standard library were for it to be integrated in the Fortran Standardization and compiler development process. So “stable” functions would be those that would never change again. I’m not sure if that is still the case, or if stdlib will become more of an numpy/scipy/boost type of library. The latter seems more likely to me. In this case, we should all expect that there will be API changes every now and then, but likely not very often.

As @hkvzjal said, you should use the “release” tag to get a certain release. If you use the stdlib-fpm (or stdlib="*" in fpm), and need stability, you should pinpoint to a given commit, because that branch is updated every time a new PR is completed. Hopefully, we will have version constraint resolution in fpm soon as well.

mmhhh… There are orange flags here for the integration into an industrial software.

The complete absence of version increment for the continuous changes is one. But OK, refering to the commits is an acceptable solution in practice.

Not separating the corrective maintenance from the development of new functionalities is a more annoying one. What if:

  • I get some version at time T1
  • I discover later on that there’s a bug around
  • I see that the bug has actually been corrected since then
  • I get the version at time T2 > T1, with the bug correction
  • this new version has also breaking changes :thinking: … Knowing that the slightest modification of a public interface is a breaking change (even if it is a private component of a public derived type, for instance).

I don’t like reading that :frowning:

I would say we can modify the workflow to suit our needs. The goal is to produce stdlib that has a good API that the community either agrees with, or at least is not against. So maybe we can simply propose to move some API to “stable” and ask in a thread here for feedback and ensure the community is on board.

1 Like

Indeed, we don’t like API changes, and this is not taken lightly. Now, here is where the eyes of the community are required to help reviewing new feature proposals such that down the road this doesn’t need to happen.

Let me take the get_line procedure for example: the previous naming did not respect the Style Guide and a new procedure was being introduced next to it mimicking the incorrect style: A debate in the PR was opened (guilty) regarding what to do, it was decided that the wrong style could be fixed to avoid future developments inherenting from it and keep things homogeneous.

There are two things that I think could be done to prevent such things from happening: 1. Encourage more caring eyes to give their opinions when a feature is under the making, especially regarding procedure signatures. Once a signature is decided it should not be changed. 2. Introduce in the CI things like pre-commit hooks, and fortitude To help catch these kind of flaws as early as possible. Open discussion on that Add module header documentation · Issue #944 · fortran-lang/stdlib · GitHub If someone has a bit more of expertise in github CI, this would be an amazing contribution, imho.

2 Likes

To clarify this:

The standard way to dealing with this is to have major.minor.patch version, and anything that is not backwards compatible must increase the major version. Anything that adds features in a backwards compatible manner increases minor and no API changes increases the patch version.

Fortran traditionally has not been changing, so if we do things right, we won’t need to increase the major version. I think we should truly aim for no changes to API. We can add new functionality with different functions, but we should never change an existing “stable” function API.

Regarding:

Since get_line is “experimental”, we can change the API. That’s what “experimental” is there for.

To make progress, let’s propose to graduate some functionality from “experimental” to “stable”.

2 Likes

It is hard to chisel an API in stone when the library is undergoing rapid development. For example one would like to add optional arguments to a procedure without breaking existing users of it. Fortran supports both positional and by-keyword actual arguments. So if someone is using positional arguments, and a new optional argument gets added, the code could break. (Unless one takes the approach that any new argument always be placed at the end of the argument list.)

What we did in ESMF was to require any optional argument use keywords. We enforced it by using a ‘keyword enforcer’ argument between the required arguments and optional ones. The ‘keyword enforcer’ is a private type, so end users can not specify it. With it in place, optional arguments can be added without API breakage.

For example:

In some module, define a keyword enforcer type within the library. The type is public within the library, but not publicized to outside callers. For example:

module myutil_mod
  implicit none
  private

! This type is public within the library, but will never be publicized outside it.
  type myutil_keywordenforcer_t
    private
    integer :: quiet
  end type
  public :: myutil_keywordenforcer_t

! These will be used both inside and outside the library
  integer, parameter, public :: mysuccess = 1, myfail = 0

end module

Another module within the library can then use these:

module mytype_mod
  use myutil_mod
  implicit none
  private

  type mytype_t
    character(20) :: myname
    integer :: myvals
  end type

  public :: mytype_t
  public :: myprint
  public :: mysuccess, myfail

contains

  subroutine myprint (this, keywordEnforcer, printstyle, errcode)
    type(mytype_t), intent(in) :: this
    type(myutil_keywordEnforcer_t), optional :: keywordEnforcer
    character(*), intent(in), optional :: printstyle
    integer, intent(out), optional :: errcode

    if (present (printstyle)) then
      select case (printstyle)
      case('titled')
        print *, 'name, value:'
      case ('untitled', ' ')
      case default
        if (present (errcode)) then
          errcode = myfail
          return
        end if
      end select
    end if

    print *, this%myname, this%myvals
    if (present (errcode)) then
      errcode = mysuccess
    end if

  end subroutine

end module

Finally, in the main program the method is called in a couple different ways. But all optional arguments must use keyword=value:

program main
  use mytype_mod
  implicit none

  type(mytype_t) :: something
  integer :: err

  something = mytype_t('xyzzy', 42)
  call myprint (something)
  ! Note that since the keyword enforcer type was never publicized, we are forced to
  ! use keyword=value arguments for printstyle and errcode:
  call myprint (something, printstyle='titled')
  call myprint (something, errcode=err)

end program

With this in place, one can add all the optional arguments one wants, and in any order, without breaking existing code.

2 Likes

I get that. However:

  • it’s sometimes possible to fix an issue in an API without breaking anything. For instance in this case by keeping the faulty getline() while adding the new get_line(). And describing only the latter in the updated doc. That way, existing codes calling getline() continue working.
  • a safe usage of the library requires to clearly know at which points an API change may occur. And it is seems that at the moment API changes can occur at any point, which is actually not safe at all for the users.

In the software of our company, if we write/integrate a library for our internal usage, the API MUST NOT change afterwards. No exception. If breaking changes are really needed, we have to duplicate the whole library and change the name (and the names of all the public entities of the library), and the old one stays “forever”. So, you really think twice(**n) before changing anything…

I don’t know if this page is up-to-date, but it looks like that everything in stdlib is considered as “experimental” at the moment? Which means that API changes could occur everywhere at any time? If it’s the case, I simply can’t integrate/use it…

And on a longer term, anything “experimental” should be in a separate branch (or separate branches). That way, the corrective maintenance would be decoupled from the ongoing developments.

This is actually not that simple. In your description, the new API is indeed backward compatible with the old one, but the new ABI is not compatible with the old one. If the library is distributed as a binary file, this is a breaking change.

1 Like

I confirm that the backward compatibility is a key point for using stdlib in an industrial context. And anything else would not be very fortranic :smile:.
Of course that can always happen and it’s sometimes better to propose something, even not optimal (whatever that means), and refine based on user feedbacks. When you get inspiration from other languages, you cannot do too wrong.

Fortran propose enough mechanisms (interfaces, generic bound procedure) to maintain multiple implementations -old and new- at least for some time. Maintenance becomes the pain point.

If I look at the way it’s done in C# for instance there are a couple of things that should be implemented to mitigate this:

  • implement systematic semantic versioning with major.minor.patch. (breaking changes are major)
  • use deprecated/obsolete attribute in the code and documentation (C# has the [Obsolete(‘Superseded by …’)] attribute). This could be a keyword in Ford (@obsolete). To see the message at compile time you can always use the non-standard directive !DIR$ MESSAGE:'OBSOLETE(foo). The function is superseded by "bar": in file __FILE__, line __LINE__ '
  • The .NET has the stable repo and there is another one called dotnext for everything experimental. New features get eventually merged once mature enough.
1 Like

You are moving the goalposts by insisting both forward and backward compatibility at the ABI level. New Fortran Standards often add optional arguments to existing intrinsics. Even moving from one compiler release to another can be a problem. (For example, gfortran has historically had issues with older .mod files being usable by newer compilers…) Shared library versioning can help with some of this.

In practice, the keyword enforcer idea worked really nicely. When we introduced them into the library, there was some initial gnashing of teeth getting existing code to conform. (We also went through the entire library to make all like-for-like keyword names consistent.) After that it was clear sailing. But ESMF has a user base who are accustomed to keeping up with new releases and recompiling from source for their particular environment.

I am not moving the goalposts, I am pointing the fact that ABI compatibility is also something to consider.

OK, but this is not a universal practice. When we update a library we distribute the binary without recompiling the entire software (I won’t go into details, but there are very good reasons for that in our case). So, an ABI change is not an option, even if the API is stable.

I can’t speak for the stdlib developers. But stdlib is open source. Anyone can download and compile it in whatever environment they are using.

That’s not the point: I do download and compile the sources.

But I am working in/on an industrial software where once compiled, the library is meant to be distributed in binary form, alongside many other libraries (internal or external ones). And if I update/recompile/redistribute the library, the rest of the software won’t be recompiled, which means that the ABI has to be stable.

Yes, right now everything is “experimental”, so indeed you should not use it in production. And that’s why I am proposing it’s time to settle some of the API. I agree with your other comment that once things are settled, we should never change the API. We have to maintain it forever. So that’s why we do not want to settle things where we have not figured out a good API, and so I think it should not be on a separate branch, but rather in the main repository and get people to use it and get some experience with it. We used to have the modules in stdlib contain the term “experimental” in their name, to drive this point home. Unfortunately I think this was changed at some point.

2 Likes

OK… Sounds like I’d better wait for a release with some stable parts.

I don’t think it’s a problem. I’m sure they are enough enthusiast people around to use an experimental branch. Many softwares use such a scheme, offering a stable version that receives only the bug corrections etc, and a beta (or whatever the name: testing, development…) version with the new functionalities that will be in the next stable version.

However, maintaining separate branches requires more work. Marking the experimental modules in their name can be a reasonable alternative (the limitation of this approach is if a part of the module is stable, and another part experimental…)

1 Like

Thank you for sharing all these pieces of information, it’s very interesting to understand how libraries (and their versions) are handled in an industrial environment and I would like to summarize more.

  • do you assume that whatever minor or patch updates do not change either ABI or API (and Fortran module files, I would say)? So that you can basically just hot swap the library file?

  • when a function changes signature (i.e. getlineget_line), for how long is the old version expected to be maintained, before it can actually be removed from the library?

Unless you want to limit yourself to the Fortran subset (or shall I say FORTRAN subset) included in the System V ABI (https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf), ABI stability is not very practical. Essentially it prevents you from using any of the advanced array features (assumed-shape arguments, assumed-rank arguments, elemental functions, …). When you do use these things, the ABI is vendor-specific (each use a different dope vector struct for instance). So I think API stability is the best stdlib can aim for (plus we distribute the source code only). But @PierU’s answer would be more helpful.