Pascal's Triangle at Compile Time in Fortran

Pascal’s triangle — also known as Khayyam’s triangle after the Persian mathematician and poet Omar Khayyam — arranges the binomial coefficients

$$\binom{n}{k} = \frac{n!}{k!\,(n-k)!}$$(1)

into a triangular grid. Each interior entry is the sum of the two entries above it. It is a lovely little object, and it turns out we can build the whole thing at compile time in modern Fortran, so that the running program does nothing but print a table of literals.

Here is the complete program:

! pascal.f90 -- prints Pascal's triangle
implicit none
integer, parameter :: nmax = 8
character(len=*), parameter :: fmt = '(9(I6,:,1X))'   ! 9 = nmax + 1
integer, parameter :: pascal(0:nmax, 0:nmax) = reshape( &
    [([( product([(i, integer :: i = n-k+1, n)])  &
       / product([(i, integer :: i = 1, k)]),     &
       integer :: k = 0, nmax)], integer :: n = 0, nmax)], [nmax+1, nmax+1])
write(*,fmt) pascal
end

Compiling and running it with flang 22.1.8:

$ flang -pedantic pascal.f90 && ./a.out
     1      0      0      0      0      0      0      0      0
     1      1      0      0      0      0      0      0      0
     1      2      1      0      0      0      0      0      0
     1      3      3      1      0      0      0      0      0
     1      4      6      4      1      0      0      0      0
     1      5     10     10      5      1      0      0      0
     1      6     15     20     15      6      1      0      0
     1      7     21     35     35     21      7      1      0
     1      8     28     56     70     56     28      8      1

How it works

The key observation is that pascal is a parameter — a named constant. Its initializer must therefore be a constant expression, which means the compiler evaluates every entry while it is compiling. At run time there is no arithmetic left to do; the executable simply prints nine rows of pre-computed integers.

Don’t take my word for it — see for yourself on Compiler Explorer. The assembly contains no loops and no multiplications, just the finished table sitting in the data section as 81 integer literals (row by row, exactly as printed):

_QQroX9x9xi4X0:
        .long   1
        .long   0
        .long   0
        ...
        .long   28
        .long   8
        .long   1

The initializer itself is just equation (1) in disguise: the first product multiplies the integers from n-k+1 up to n, the second gives the factorial of k, and their integer quotient is the binomial coefficient — the division is always exact. Two edge cases fall out for free. For k = 0 both ranges are empty, and the empty product is 1 — exactly the value we want. And for k > n the first range runs through zero, so the numerator vanishes — those are the zeros filling the upper half of the table. Finally, with k as the innermost implied-do index the flat list comes out one triangle row after another, which is precisely the storage order in which write prints the reshaped array.

Why Fortran 2018?

The youngest ingredient is declaring the implied-do index inside the array constructor — the integer :: i = ... and friends above. Fortran 2008 allowed this for do concurrent and forall; Fortran 2018 extended it to array constructors and data statements (§5.18 of John Reid’s The New Features of Fortran 2018). As Steve Lionel explains on Fortran Discourse, the index is a construct-scope integer regardless — the real novelty is stating its kind in place, so no integer :: i, k, n declarations clutter the surrounding scope. On paper the feature is from 2018; in practice the state of affairs is rather sad. Of the compilers I tried, gfortran 16.1, lfortran 0.59, nvfortran 26.5, and NAG 7.2 all reject the program, and ifx 2025.3 compiles it but prints a wrong table — leaving flang as the only compiler I know of that gets it right.

A note on the format

The edit descriptor '(9(I6,:,1X))' prints nmax + 1 integers per line in six-character fields, separated by a space; the colon (:) stops the format once the data list is exhausted. The repeat count 9 is the one thing that does not follow nmax automatically — change one and you must touch the other, as there is no convenient way to splice an integer into a character constant expression.

Further reading

Compile-time evaluation is still young territory in Fortran. A few more explorations of what is possible today:

And if you enjoy the triangle itself, it stars in one of my favourite Veritasium videos, The Discovery That Transformed Pi — highly recommended.

#Fortran   #Compile-Time   #Metaprogramming