LFortran User Guide

About LFortran

LFortran’s goal is an implementation of the latest Fortran standard (currently F2018) with some added extensions. It works on Linux, macOS, most BSDs and on Windows.

The LFortran compiler consists of the following components:

  • The LFortran compiler itself.

  • A runtime library supporting language elements and intrinsic functions.

  • Several module definitions which define the external environment and supply particular details about the computer running the program.

  • The capability to read additional module definitions which define the interfaces to external libraries such as OpenMPI.

  • Generation of executable code with the LLVM compiler infrastructure.

  • Generation of C, C++, WebAssembly or Julia code.

  • Interactive usage via LLVM JIT

  • Source file formatting (lfortran fmt).

Standards

The LFortran compiler is currently in the alpha stage and is actively under development. It is designed to support the following Fortran standards and modes:

Supported Standards:

  • ISO Fortran 2023 (--std=f23): Partial support for the latest Fortran 2023 features. Use this option to enable conformance to Fortran 2023 where supported. Conditional expressions (10.1.2.3), written ( cond ? a : b ), are supported and are accepted in every mode. They are executable expressions only: 10.1.11 and 10.1.12 do not list a conditional expression among the primaries a specification expression or a constant expression may contain, so it is rejected in a KIND=, a LEN=, an array bound and an initialization expression; use merge there. Conditional arguments (15.5.1, 15.5.2.3) are supported too. In an actual argument position the same syntax selects the actual argument itself, so a consequent that is a variable is passed by reference and stays definable, and a consequent of .NIL. leaves an optional dummy argument not present: call sub( ( x<5 ? x : y ), ( have_edge ? edge : .NIL. ) ).

  • Legacy Mode (--std=legacy): Allows compilation of older Fortran codebases that use non-standard or deprecated syntax.

  • LFortran Mode (--std=lf): Default mode focusing on modern Fortran features, including experimental extensions beyond the current standards.

Extensions

The extensions are currently in development and are planned to include:

  • Global Scope (statements outside of a program block).

  • Interactive Fortran (in alpha).

  • Jupyter integration.

  • Support for GPUs and other accelerators.

  • Type inference (:= operator and --infer mode).

Type Inference with :=

Experimental, subject to change.

The := operator declares a new variable whose type is inferred from the right-hand side. It is closely inspired by Go’s short variable declaration (:=), where it is the dominant way to declare local variables. The rationale is the same: when the right-hand side already communicates the type, repeating it on the left is redundant noise. := makes declarations visible at a glance without requiring the programmer to spell out types that the compiler can deduce.

:= is not standard Fortran syntax. It is an LFortran-specific extension, and the compiler emits a warning on each use to make this clear. The --infer flag suppresses the warning.

x := 42              ! integer
y := 3.14d0          ! real(8)
z := (1.0d0, 2.0d0)  ! complex(8)
vals := [1, 2, 3]    ! integer, dimension(3)
p := point_t(0.0, 0.0)  ! type(point_t)

Rules

:= always declares a new variable. It never assigns to an existing one.

x := 5
x = 10     ! ok: plain assignment to existing variable
x := 10    ! error: x is already declared in this scope
integer :: x
x := 5     ! error: x is already declared

Inner blocks may shadow outer variables:

x := 5
block
    x := "hello"   ! ok: new x in inner scope
end block
! outer x is still integer 5

Supported right-hand sides: integer, real, complex, logical, character scalars; fixed-size arrays when dimensions are compile-time constants; derived types via structure constructors or typed expressions. The inferred kind matches the expression (3.14d0 produces real(8)).

Guidance

Use := when the type is evident from the right-hand side:

cfg := load_config("input.nml")
origin := point_t(0.0, 0.0)
coeffs := [1.0d0, 0.5d0, 0.25d0]

Use explicit declarations when the type matters for clarity or safety:

real(dp) :: result
result = compute(data)    ! reader sees the type; catches signature changes

integer :: i, n           ! simple counters are clearer with explicit types

--infer mode

--infer enables a second, more permissive form of inference: the first plain = to an undeclared name declares it, like a scripting language. This is intended for interactive and notebook use.

Feature

Syntax

Declares

Intended for

:=

x := expr

Always

Production code

--infer

x = expr

On first use

Interactive / notebooks

:= works independently of --infer. When both are active, either form declares, but := makes the intent explicit.

Interactive Compiler

LFortran supports an interactive mode; just run the lfortran command to start it.

Sharing a session

The prompt is a Jupyter client. It runs its cells in a Jupyter kernel that it starts, and it tells where the kernel’s connection file is, so that other clients can join the session, with its variables, procedures and modules: another prompt, jupyter console --existing FILE or any Jupyter client.

$ lfortran
...
  * Others can join with `lfortran --connect /tmp/lfortran-0123abcd.json`
>>>
$ lfortran --connect /tmp/lfortran-0123abcd.json

A kernel can also be started on its own; it writes the connection file if it does not exist and runs until a client shuts it down, or until Ctrl-C or SIGTERM:

$ lfortran kernel -f session.json
$ lfortran --connect session.json

The kernel runs one cell at a time, in the order the cells arrive. Clients subscribe to every cell’s output, as in Jupyter: at a terminal, the prompt shows another client’s cell, marked [other client], and its published output even while idle or editing, preserving the current input and cursor. Redirected input retains blocking reads; it does not refresh while waiting for input. Output fragments share a transcript line until a newline; a carriage return moves within that line without erasing its untouched suffix. Wrapping uses LLVM’s Unicode terminal-cell widths, including wide CJK glyphs and zero-width combining marks, rather than counting UTF-8 bytes or scalars. Terminal-specific emoji/grapheme and ambiguous-width settings may differ from these widths. The editor supports SGR styling, not arbitrary terminal control sequences. Unsupported controls, invalid UTF-8, and unfinished sequences when leaving a cell produce an explicit error. Retained incomplete sequences, styling metadata per refresh, and a final-cell combining cluster are each limited to 64 KiB; rejection restores terminal mode and cursor visibility. A cell’s output is shown when the cell finishes or reads input. A cell that fails aborts the cells waiting behind it, unless it was sent with stop_on_error false. Delivery is not guaranteed if a client cannot consume output fast enough.

A READ asks the client that ran the cell. If that client disconnects, the READ fails with an I/O error: a READ with iostat= gets it and the cell goes on, but any other READ stops the kernel with a runtime error, as it stops a program. STOP, ERROR STOP and a crash stop the kernel too. A kernel that stops ends the session of every client; the prompt that started it then exits with the kernel’s exit status, as it did when it ran its cells itself.

The prompt that starts a kernel ends that kernel when the prompt exits, and removes the connection file. Exiting ends the kernel while another client is still connected, and during a cell that does not return. Ctrl-C ends the prompt and the kernel. The kernel also stops, removing the connection file, when that prompt cannot end it, such as when its terminal closes or it is killed with kill -9. A prompt that only joins, with lfortran --connect FILE, leaves the kernel running; that session lasts until the kernel itself stops. The way to stop a cell that does not return is to end the prompt that started the kernel. Jupyter’s Interrupt has no effect until the cell ends. Whoever can read the connection file can run code as its owner. The file is readable by its owner only.

The client is LFortran’s own. The kernel is LFortran’s own implementation of the Jupyter messaging protocol (ZeroMQ over TCP with HMAC-SHA256 signatures), which needs no third-party library. In a build with WITH_XEUS=yes, lfortran and lfortran kernel both run the Xeus kernel, and the prompt still ends that kernel when it exits. The Xeus kernel uses Xeus’s own server for every channel. A READ whose client has disconnected waits there, and the clients behind that cell wait with it. A cell that fails aborts the requests already waiting with an error reply and no idle status, so a client that waits for the aborted cell to become idle waits with it. See src/lfortran/jupyter.cpp and src/lfortran/fortran_kernel.cpp.

Invoking LFortran

The LFortran compiler supports numerous command-line flags to select compilation options, output options, link options and so on.

Compiler information

  • --print-targets, Print the registered CPU targets

  • --version, Show the current version

Source code format

  • --fixed-form, Parse the file assuming Fortran 66 format (6 spaces)

  • --fixed-form-infer, Use heuristics to infer if a file is in fixed form

Source code processing

  • --cpp, Enable C preprocessing

  • -E, Preprocess only; do not compile, assemble or link

Other inputs

  • -D <macro>=<value> Define a macro (or 1 if omitted)

  • -I <value>, Include path for include statements

  • -L <value>, Library path for shared libraries

  • -l <value>, Link library naming a linkable shared library

Compiler feature selections

  • --fast, Optimize for best performance on the host CPU (disable strict standard compliance). This implies native CPU selection unless --march, --mcpu, --mtune, or --target is specified. Without --fast, the LLVM backend runs no optimizations, like -O0 in Clang and Flang (WebAssembly targets keep LLVM’s default code generation level, which keeps the number of locals per function within engine limits); -O<level> is currently accepted and ignored.

  • --implicit-argument-casting, Allow implicit argument casting

  • --implicit-interface, Allow implicit interface

  • --implicit-typing, Allow implicit typing

  • --openmp, Enable OpenMP

  • --print-leading-space, Print leading white space if format is unspecified

  • --realloc-lhs-arrays, Reallocate left hand side automatically for arrays

  • --disable-realloc-lhs-arrays, Disables reallocating left hand side automatically for arrays

  • --target <value>, Generate code for the given target

  • -sSETTING[=VALUE] or -s SETTING[=VALUE], Forward a setting to the Emscripten driver when linking with an LLVM-based backend and --target=wasm32-unknown-emscripten. Repeat for multiple settings, for example -sMODULARIZE -sEXPORT_ES6. User settings follow the default stack and memory settings, so they can override them. Quote list or string values as single shell arguments, for example "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']". These settings are not host linker flags; -W forwarding is unchanged.

  • --march <value>, Select the instruction-set architecture. Use native for the host ISA.

  • --mcpu <value>, Select both the CPU instruction set and scheduling model. Use native for the host CPU.

  • --mtune <value>, Select the CPU scheduling model without changing the instruction set. Use native for the host CPU.

  • --backend flag is used to specify the target backend for code generation in LFortran. The supported backends are:

    • llvm: The most advanced and default backend, used for generating LLVM IR or machine code through LLVM.

    • wasm: For generating Webassembly via our custom wasm backend.

    • c: For generating C code.

    • cpp: For generating C++ code (requires the Kokkos library).

    • x86: For generating x86 machine code directly (without LLVM).

    • fortran: For generating Fortran code.

    • julia: For generating Julia code.

Compiler text outputs

  • --error-format <value>, Control how errors are produced (human, short)

  • --no-error-banner, Turn off error banner

  • --no-warnings, Turn off all warnings

  • -S, Emit assembly, do not assemble or link

  • --time-report, Show compilation time report

  • -v, Be more verbose

Compiler binary outputs

  • -c, Compile and assemble, do not link

  • --separate-compilation, Generate object code into .o files

  • -J <value>, Where to save mod files

  • -o <value>, Specify the file to place the compiler’s output into

  • --static, Create a static executable

Compiler debugging

A number of command-line options select various text outputs useful for debugging the compiler. See lfortran --help for a list.

Examples

The following commands and code demonstrate basic operation of the compiler.

lfortran helloworld.f90
Hello World!

lfortran -o hw helloworld.f90
./hw
Hello World!

cat helloworld.f90
program hello_world
    implicit none
    write (*, *) 'Hello World!'
end program hello_world

Here is a simple example with a module:

lfortran -c varray.f90
lfortran usev.f90
 sum is    7.20000000e+01

cat varray.f90
module varray
    integer :: nsize
end module varray

cat usev.f90
program usev
    use varray
    real, allocatable, dimension(:) :: A
    integer :: i
    nsize = 8
    allocate(A(nsize))
    do i = 1, nsize
        A(i) = 2.0*i
    end do
    print *, " sum is ", (A(1)+A(nsize))*nsize/2.0
    deallocate(A)
end program usev

The compile command for the module requires -c to avoid automatic running of the code.

Formatting Fortran source files

The lfortran compiler will automatically format source files with the fmt option. You can select auto-indent for modules, and in-place update of the Fortran source file with the -i option (use with caution!).

lfortran fmt varray.f90
module varray
integer :: nsize
real, allocatable, dimension(:) :: A
end module varray

Or add spaces and indentation as follows:

lfortran fmt --spaces 4 --indent-unit varray.f90
module varray
    integer :: nsize
    real, allocatable, dimension(:) :: A
end module varray

Selecting the C Compiler

The C backend (--backend=c) uses the same C compiler for compiling generated C and linking the executable. It selects LFORTRAN_CC when set, otherwise CC, otherwise gcc. This honors the compiler selected by a build environment such as Pixi. Use LFORTRAN_CC to override that selection for LFortran alone:

CC=clang lfortran --backend=c hw.f90 -o hw
LFORTRAN_CC=gcc lfortran --backend=c hw.f90 -o hw

The C and C++ backends also use this selection when compiling empty C objects for module-only source files, retaining cc as the default when neither variable is set. This does not change the C++ compiler used for generated C++.

The LLVM backend’s native linker selection is separate: it defaults to clang and can be changed with --linker / --linker-path or the LFORTRAN_LINKER / LFORTRAN_LINKER_PATH environment variables.

Native LLVM OpenMP programs use Clang with LLVM’s libomp. Pass --openmp during both compilation and linking, and --openmp-lib-dir=/path/to/lib when linking. The Pixi llvm_omp integration backend uses the selected environment’s lib directory.

Differences from other compilers

GNU, Intel and LLVM Fortran use “standard” Fortran carriage control where the first character of each output line controls a conceptual “line printer”. A space ” ” means single-space, a zero “0” means double-space and one “1” means form-feed before printing. This is obsolete and LFortran omits this unless --print-leading-space is selected at compile-time.

LFortran does not currently expose arbitrary Clang options such as -flto. Use --fast and the target-selection options above for native optimization.

GNU extension declarations real*8 xvalue are accepted but deprecated. This was never standard-conforming Fortran; it is an old IBM extension that predates Fortran-77.