LFortran can now compile fpm, the Fortran Package Manager. We opened up an issue for it in April 2025, when we started focusing on it as a priority. We closed it on February 7, 2026. fpm is the most complex project that we now successfully built and run. It is an interesting project: it is not computational, but rather a system project which exercises running other programs, reading environment variables, command-line argument parsing, reading and writing files, directories, parsing enough Fortran to understand dependencies, etc. It uses almost all modern Fortran features: classes, inheritance, allocatable components, constructors, arrays of classes, select type, associate, automatic LHS (left-hand side) reallocation, strings, arrays of strings and it exposed dozens and dozens of bugs and missing features in LFortran. And we have now implemented them all. As a result, LFortran is really close to beta, and we are advancing our progress bar to 9/10.

When we started our progress bar towards beta, we thought that compiling 10 codes with increasing complexity and ending with fpm would be enough for beta. But when we got close to compiling fpm, we realized we are still not quite there. Beta quality to us means that when you use LFortran on your code of N lines (say N=10,000), it will compile and run it correctly in about 90% of the cases. We are estimating that we are there with N=500 or maybe N=1,000, but not for larger codes yet. However the bugs are very small, and so we are sprinting by compiling more and more codes and fixing all bugs, which are now simple to fix.

So we decided to cheat a little bit and did not progress the bar on compiling LAPACK, which allows us to progress the bar now and still have one step left and we will progress it to beta once we are confident that we can compile medium-sized codes with high probability.

One of the big subprojects was to completely refactor how we handle classes, virtual functions and inheritance. We studied how Clang handles classes in C++ and implemented a similar approach with some Fortran-specific additions.

As with any big projects that we compile, we got it working a few weeks ago, we set up testing that compiles and runs all fpm tests at our CI (Continuous Integration) for every commit and then we keep fixing bugs from other projects and watch the CI if it ever fails in fpm with flaky bugs. If it does not after lots of other PRs (Pull Requests) are merged, we know that it is working and that it does not have simple flaky bugs. It might still have some bugs that do not show up easily, but all the things that we can see are rock solid, so we are comfortable announcing it.

We also added support for fpm’s five key dependencies: M_CLI2 command-line argument parsing), toml-f (reads TOML configuration files), fortran-regex (handles pattern matching), fortran-shlex (processes shell-like syntax), and Jonquil (manages JSON data).

Build

To build, do:

git clone https://github.com/fortran-lang/fpm
git checkout d0f89957541bdcc354da8e11422f5efcf9fedd0e  # latest main
conda create -n fpm lfortran=0.60.0 fpm gfortran
conda activate fpm
fpm --compiler=lfortran test --flag --cpp --flag --realloc-lhs-arrays --flag --use-loop-variable-after-loop

You can also append --fast or --separate-compilation options, both work as well. The gfortran compiler is necessary to pass tests, it is not being used to build any source code. Upstream would need to be improved to not have gfortran hard-coded in testing.

LFortran by default has bounds checking on, and it will give runtime errors when an incorrect shape or an unallocated array is used. This found a genuine upstream bug that we fixed by submitting a PR upstream. Over time we will harden our compile-time and runtime checks so that your code either works or gives an error at compile-time or runtime, but never segfaults or has other undefined behavior.

Speed of Compilation

To get some idea about the speed of compilation, here are the times on Apple M4 MacBook Pro:

$ time fpm build --compiler=gfortran
[...]
libfpm.a                               done.
main.f90                               done.
fpm                                    done.
[100%] Project compiled successfully.

real    42.342s
user    39.902s
sys     2.050s
cpu     99.1%
$ time fpm --compiler=lfortran build --flag --cpp --flag --realloc-lhs-arrays --flag --use-loop-variable-after-loop
[...]
real    16.641s
user    15.533s
sys     0.968s
cpu     99.2%

The exact compiler versions are:

$ gfortran --version
GNU Fortran (conda-forge gcc 15.2.0-18) 15.2.0
$ lfortran --version
LFortran version: 0.60.0
Platform: macOS ARM
LLVM: 19.1.1
Default target: arm64-apple-darwin24.6.0
LSP Version: 3.17.0
JSON_RPC Version: 2.0

Both are single-core compilation of the fpm binary. Overall, not bad for a start.

To get a better idea where LFortran is spending compilation time, you can rerun fpm with --verbose and then manually run longest compilation command, which happens to be:

lfortran -c app/main.f90  --cpp --cpp --realloc-lhs-arrays --use-loop-variable-after-loop -DFPM_RELEASE_VERSION=0.12.0 -Ibuild/dependencies/fortran-regex/src -J build/lfortran_29F2E0FA2D75FE0A -Ibuild/lfortran_29F2E0FA2D75FE0A -Ibuild/lfortran_5D5DD1C987059777 -o build/lfortran_29F2E0FA2D75FE0A/fpm/app_main.f90.o --time-report

Notice the --time-report option that we added at the end. This prints:

Allocator usage of last chunk (MB)                     0.284
Allocator chunks                                       7.000
------------------------------------------------------------
Component name                                     Time (ms)
------------------------------------------------------------
File reading                                           0.450
Src -> ASR                                            93.267
ASR passes (total)                                   178.630
    global_stmts                                       0.000
    init_expr                                          1.885
    function_call_in_declaration                       2.686
    openmp                                             0.000
    implied_do_loops                                   4.687
    array_struct_temporary                            13.439
    transform_optional_argument_functions             11.940
    nested_vars                                        8.111
    forall                                             1.281
    class_constructor                                  4.553
    pass_list_expr                                     4.867
    where                                              1.652
    array_op                                           4.886
    symbolic                                           0.708
    intrinsic_function                                29.437
    intrinsic_subroutine                               5.336
    subroutine_from_function                           7.500
    array_op                                           5.352
    pass_array_by_data                                16.393
    array_passed_in_function_call                      6.463
    print_list_tuple                                   1.797
    array_dim_intrinsics_update                        6.731
    do_loops                                           6.631
    while_else                                         3.165
    select_case                                        3.459
    unused_functions                                  21.647
    unique_symbols                                     0.000
    insert_deallocate                                  3.432
    other processing time                              0.026
ASR -> mod                                             0.410
LLVM IR creation                                     329.712
LLVM opt                                               0.000
LLVM -> BIN                                        12539.240
------------------------------------------------------------
Total time                                         13204.171
------------------------------------------------------------

Here, ASR means Abstract Semantic Representation, the main internal representation (IR) that LFortran uses. We then run a similar line to figure out how long it takes to link.

So here is the breakdown:

Task Time [s] Percentage
Compiling all files to one ASR 3.5s 21.1%
ASR->ASR passes 0.2s 1.2%
ASR->LLVM IR 0.3s 1.8%
LLVM IR -> object file 12.5s 75.3 %
Linking 0.1s 0.6%
Total 16.6 100%

The main bottleneck is LLVM IR compilation to binary object code (over 3/4 of all the time). This excludes creating the LLVM IR from ASR as well as linking (both counted on separate lines above). To speed this up, we plan to create our own backend later, we think it might be possible to make it compile about 10x faster, which would bring the total time down to about 5s.

After that, we can start optimizing the other parts. The first line, compilation of all files to one ASR, includes parsing all files, doing semantic analysis, loading all dependent Fortran modules’ .mod files, and saving a .mod file for a given module.

That is not the most efficient way to compile it: a better way would be for the build system (fpm in this case) to give LFortran the dependency graph and files to compile and compiler options for each file and LFortran would compile all the files at once, never saving or loading .mod files from the disk, assembling the final ASR step by step.

It is a very interesting computational problem of taking source code and emitting a binary as fast as we can, and we are excited to start working on that after we reach beta.

Going Forward

Now that we reached this big milestone, we are working on other third-party codes and fixing all bugs that we encounter. We are still in alpha stage. When we feel comfortable expecting LFortran to just work on medium-sized codes, we will announce beta quality. It is becoming easier and easier to compile a new third-party code, the bugs are getting smaller and less frequent. As far as we know, all Fortran features are implemented, except coarrays and parametrized derived types (PDT), which we will tackle after beta. It is hard to predict a timeline, but our best guess is a timeframe of several months (so not weeks, but also not years). We will see if we were right.

We will keep announcing every new significant third-party code that we can compile.

Join Us

We welcome new contributors to join our journey. If you’re interested, please reach out on Zulip. Working on LFortran is both challenging and rewarding, offering great opportunities for learning and growth.

Acknowledgements

We want to thank:

Discussions