Fortran has powered simulations of galaxies, weather systems, and quantum phenomena for over seven decades. Its enduring strength lies in its clarity, performance, and mathematical soul—qualities that resonate deeply with its community of developers. Yet, nestled within this ecosystem is a contentious tool: the preprocessor. From its ad hoc beginnings in the 1970s to its modern incarnations in tools like cpp, fpp, and fypp, preprocessing has been both a lifeline and a lightning rod for Fortran developers. It enables portability across diverse platforms, conditional compilation for debugging, and code generation for complex libraries—capabilities critical to Fortran’s role in high-performance computing. But it also sparks fierce debate, with many Fortraners decrying its tendency to obscure code, disrupt the language’s elegant simplicity, and introduce bugs that haunt scientific precision. This article explores the pivotal uses of preprocessing in Fortran, delving into the passionate love-hate relationship that defines its place in the community—a tug-of-war between pragmatic necessity and a purist’s devotion to Fortran’s unadulterated clarity.
Fortran, born in the 1950s for scientific and numerical computing, was designed for clarity and performance on early computers. Preprocessing was not part of its original vision. As Fortran evolved, the need for portability, code reuse, and conditional compilation grew, particularly in large-scale scientific projects. This led to the adoption of preprocessing tools, though their integration into Fortran’s ecosystem has been uneven and controversial. Below is a concise history of preprocessing in Fortran, culminating in the notable attempt to standardize it with CoCo.
In Fortran’s infancy (Fortran I, II, IV), preprocessing was virtually nonexistent. Developers relied on manual code edits or rudimentary scripts to handle tasks like platform-specific tweaks. Early computers varied widely in architecture, so scientists often customized code by hand for each system—a tedious process. Some used external tools, like simple text processors, to automate repetitive changes, but these were bespoke and non-standard. Fortran 66 and 77, with their rigid structure, offered no built-in preprocessing capabilities, leaving developers to cobble together solutions.
By the 1980s, Fortran 77 was the workhorse of scientific computing, and large projects—like climate models or finite element simulations—demanded portability across diverse hardware (e.g., Cray, VAX, IBM). The C preprocessor (cpp), developed for C, became a popular stopgap. Its #define, #ifdef, and #include directives allowed Fortran developers to write flexible code for multiple platforms. Files with .F or .F77 extensions signaled preprocessing, distinguishing them from raw .f files. However, cpp was a imperfect fit: its C-centric syntax clashed with Fortran’s column-based formatting, and it could mangle Fortran’s fixed-form source, leading to errors. Dedicated Fortran preprocessors, like fpp, emerged to address these issues, offering better integration but lacking universal adoption.
Fortran 90 introduced modules, parameterized types, and dynamic memory, giving developers native tools for modularity and portability. These features reduced reliance on preprocessing for tasks like code reuse or constant definition. For example, modules replaced many #include use cases, and PARAMETER statements handled constants better than #define. Still, preprocessing persisted in large codebases, especially for conditional compilation (e.g., enabling/disabling debug code) or legacy Fortran 77 projects. Tools like cpp and fpp remained common, though their use was often seen as a necessary evil due to debugging challenges and code obfuscation.
By the late 1990s, the Fortran community recognized preprocessing’s utility but also its chaos. Different preprocessors (cpp, fpp, custom tools) produced inconsistent behavior, and there was no standard way to write portable, preprocessable Fortran code. This led to the development of CoCo (Conditional Compilation), a proposed standard for Fortran preprocessing, spearheaded by the ISO/IEC Fortran committee (J3).
CoCo aimed to integrate preprocessing directly into the Fortran language, defining a native syntax for directives like conditional compilation, macro expansion, and file inclusion. Unlike cpp, CoCo was designed with Fortran’s structure in mind, respecting its free- and fixed-form source and avoiding C’s pitfalls. Key goals included:
CoCo’s syntax used directives prefixed with ??, such as ??IF, ??DEFINE, or ??INCLUDE, to distinguish them from Fortran code. For example:
This allowed conditional compilation without external tools, and compilers could process it natively.
However, CoCo never made it into the Fortran standard. Several factors contributed:
cpp and fpp were entrenched, and many developers saw no need for a new standard when workarounds existed.By the early 2000s, CoCo faded as a proposal. The Fortran 2003 standard focused on object-oriented features and interoperability (e.g., with C), sidelining preprocessing standardization.
Today, preprocessing in Fortran remains unstandardized but widely used. cpp and fpp are still common, especially in high-performance computing (HPC) projects like LAPACK, PETSc, or WRF, hosted on platforms like GitHub. Newer tools, like fypp (a Python-based preprocessor), have gained traction for their flexibility, particularly in projects like the Fortran Standard Library. Fortran 2008, 2018, and the upcoming 2023 standard introduced features like submodules and enhanced generics, further reducing preprocessing needs, but legacy code and cross-platform projects keep preprocessors relevant.
The CoCo attempt left a legacy: it highlighted the community’s ambivalence toward preprocessing. While some developers value its power, others see it as a last resort, preferring Fortran’s native constructs. The lack of a standard means preprocessing remains a fragmented practice, with tools and conventions varying by project.
Preprocessing in Fortran evolved from ad hoc scripts to external tools like cpp, driven by the need for portability and flexibility in scientific computing. The CoCo initiative sought to bring order by standardizing preprocessing, but resistance from a community valuing simplicity, combined with practical hurdles, led to its demise. Today, preprocessing endures as a pragmatic tool in Fortran’s ecosystem, neither fully embraced nor abandoned, reflecting the language’s balance between tradition and adaptation.
In modern Fortran (referring to standards like Fortran 2003, 2008, 2018, and the upcoming 2023), preprocessing remains a practical tool despite the language’s evolution toward native constructs that reduce its necessity. While modern Fortran offers robust features like modules, derived types, and submodules, preprocessing is still widely used in specific scenarios, particularly in large-scale scientific, high-performance computing (HPC), and legacy projects. Below, I’ll expand on the most common uses of preprocessing in modern Fortran, focusing on their practical applications, prevalence, and integration with contemporary development practices, as seen in open-source projects and industry.
What It Is: Preprocessing directives like #ifdef, #ifndef, #else, and #endif allow developers to include or exclude code blocks based on predefined conditions, typically set at compile time.
Common Uses:
NEW_SOLVER is defined.Prevalence:
Why It Persists:
IF statements can handle runtime conditions, they don’t exclude code from compilation, which impacts binary size and performance optimization.What It Is: Preprocessing helps write code that adapts to different compilers, operating systems, or hardware by defining platform-specific macros or including system-dependent code.
Common Uses:
gfortran, Intel ifort, NVIDIA nvfortran) support unique extensions or optimizations. Preprocessing ensures compatibility.KIND system allows flexible numeric precision, but preprocessing is used to enforce consistent precision across platforms or to switch between single/double precision for performance.Prevalence:
Why It Persists:
ISO_C_BINDING and improved interoperability, hardware and library ecosystems remain fragmented. Preprocessing bridges these gaps more efficiently than rewriting code for each target.What It Is: The #include directive inserts external files into the source code, often for shared definitions, constants, or interfaces.
Common Uses:
constants.h might contain: #include to share COMMON blocks or subroutine declarations.#include pulls in header-like files defining data structures or function signatures.Prevalence:
#include persists in mixed-language or older HPC codes.Why It Persists:
#include is entrenched in legacy workflows and simpler for small, shared snippets. It’s also used when interfacing with C-style build systems.What It Is: Macros (#define) create reusable code snippets or constants, reducing duplication or simplifying complex expressions.
Common Uses:
PARAMETER is preferred, macros define constants in preprocessed code, especially for conditional values.fypp generate boilerplate code, such as loops or type-specific routines, for generic programming.fypp): Prevalence:
fypp is gaining traction in newer projects like the Fortran Standard Library (stdlib) for its code-generation capabilities.Why It Persists:
fypp extend this to template-like programming, which Fortran’s generics don’t fully replicate. However, overuse is frowned upon due to readability concerns.What It Is: Preprocessing integrates Fortran code with build systems (e.g., CMake, Autotools) or external tools by defining flags or generating code.
Common Uses:
fypp or custom scripts generate Fortran code for specific cases (e.g., different precisions or dimensions), embedded in the build process.Prevalence:
Why It Persists: