1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
|
Assert
======
An assertion utility that combines variable stop codes and error termination in `pure` procedures to produce descriptive messages when a program detects violations of the requirements for correct execution.
Motivations
-----------
1. To mitigate against a reason developers often cite for not writing `pure` procedures: their inability to produce output in normal execution.
2. To promote the enforcement of programming contracts.
Overview
--------
This assertion utility contains four public entities:
* An `assert_macros.h` file defining the recommended preprocessor macros for writing assertions:
- `call_assert(assertion)`
- `call_assert_describe(assertion, description)`
* An `assert` subroutine
The `assert` subroutine
* Error-terminates with a variable stop code when a caller-provided logical assertion fails,
* Is callable inside `pure` procedures, and
* Can be eliminated by not defining the `ASSERTIONS` preprocessor macro.
Assertion enforcement is controlled via the `ASSERTIONS` preprocessor macro,
which can be defined to non-zero or zero at compilation time to
respectively enable or disable run-time assertion enforcement.
When the `ASSERTIONS` preprocessor macro is not defined to any value,
the default is that assertions are *disabled* and will not check the condition.
To enable assertion enforcement (e.g., for a debug build), define the
preprocessor ASSERTIONS to non-zero, e.g.,
```
fpm build --flag "-DASSERTIONS"
```
The program [example/invoke-via-macro.F90] demonstrates the preferred way to invoke assertions via the three provided macros.
Invoking assertions this way ensures such calls will be completely removed whenever the `ASSERTIONS` macro is undefined (or defined to zero) during compilation.
Due to a limitation of `fpm`, this approach works best if the project using Assert is also a `fpm` project.
If instead `fpm install` is used, then either the user must copy `include/assert_macros.h` to the installation directory (default: `~/.local/include`) or
the user must invoke `assert` directly (via `call assert(...)`).
In the latter approach when the assertions are disabled, the `assert` procedure will start and end with `if (.false.) then ... end if`, which might facilitate automatic removal of `assert` during the dead-code removal phase of optimizing compilers.
Use Cases
---------
Two common use cases include
1. [Supporting output in pure procedures](#supporting-output-in-pure-procedures) for debugging purposes.
2. [Enforcing programming contracts](#enforcing-programming-contracts) throughout a project via runtime checks.
### Supporting output in pure procedures
Writing pure procedures communicates useful information to a compiler or a developer.
Specifically, the pure attribute conveys compliance with several constraints that clarify data dependencies and preclude most side effects.
For a compiler, these constraints support optimizations, including automatic parallelization on a central processing unit (CPU) or offloading to a graphics processing unit (GPU).
For a developer, the constraints support refactoring tasks such as code movement.
The Fortran standard prohibits input or output in pure procedures, which precludes a common debugging mechanism.
A developer seeking output inside a procedure presumably has an expectation regarding what ranges of output values represent correct program execution.
A developer can state such expectations in an assertion such as `call_assert(i>0 .and. j<0)`.
Enforce the assertion by defining the `ASSERTIONS` macro when compiling.
If the expectation is not met, the program error terminates and prints a stop code showing the assertion's file and line location and a description.
By default, the description is the literal text of what was asserted: `i>0 .and. j<0` in the aforementioned example.
Alternatively, the user can provide a custom description.
### Enforcing programming contracts
Programming can be thought of as requirements for correct execution of a procedure and assurances for the result of correct execution.
The requirements and assurances might be constraints of three kinds:
1. **Preconditions (requirements):** `logical` expressions that must evaluate to `.true.` when a procedure starts execution,
2. **Postconditions (assurances):** expressions that must evaluate to `.true.` when a procedure finishes execution, and
3. **Invariants:** universal pre- and postconditions that must always be true when all procedures in a class start or finish executing.
The [example/README.md] file shows examples of writing constraints in notes on class diagrams using the formal syntax of the Object Constraint Language ([OCL]).
Running the Examples
--------------------
See the [./example](./example) subdirectory.
Building and Testing
--------------------
- [General Build Knobs](#general-build-knobs)
- [Cray Compiler Environment (CCE) `ftn`](#cray-compiler-environment-cce-ftn)
- [GNU Compiler Collection (GCC) `gfortran`](#gnu-compiler-collection-gcc-gfortran))
- [Intel `ifx`](#intel-ifx))
- [LFortran `lfortran`](#lfortran-lfortran)
- [LLVM `flang-new`](#llvm-flang-new)
- [Numerical Algorithms Group (NAG) `nagfor`](#numerical-algorithms-group-nag-nagfor)
### General Build Knobs
The following build-time preprocessor knobs can be used to control the behavior of Assert.
When using `fpm` to build, these boolean flags can be passed on the command-line
using syntax like: `fpm --flag "-DASSERTIONS=1"`
* `ASSERTIONS` : Controls the whether assertions are checked/enforced at runtime.
The default is 0 (assertions disabled). Assertions can be enabled using `-DASSERTIONS=1`.
* `ASSERT_MULTI_IMAGE`: Controls whether the library attempts to use multi-image
Fortran features (e.g. to report the image number of an assertion failure).
The default is compiler-specific. Multi-image support can be disabled using
`-DASSERT_MULTI_IMAGE=0`.
* `ASSERT_PARALLEL_CALLBACKS`: Controls the use of a callback interface for
multi-process features. Contact us for more details.
### Cray Compiler Environment (CCE) `ftn`
Because `fpm` uses the compiler name to determine the compiler identity and because
CCE provides one compiler wrapper, `ftn`, for invoking all compilers, you will
need to invoke `ftn` in a shell script named to identify CCE compiler. For example,
place a script named `crayftn.sh` in your path with the following contents and with
executable privileges set appropriately:
```
#!/bin/bash
ftn $@
```
Then build and test Assert with the command
```
fpm test --compiler crayftn.sh --profile release
```
### GNU Compiler Collection (GCC) `gfortran`
#### Single-image (serial) execution
With `gfortran` 14 or later, use
```
fpm test --profile release
```
With `gfortran` 13 or earlier, use
```
fpm test --profile release --flag "-ffree-line-length-0"
```
The above commands build the Assert library (with the default of assertion enforcement disabled) and runs the test suite.
#### Multi-image (parallel) execution
With `gfortran` 14 or later versions and OpenCoarrays installed, use
```
fpm test --compiler caf --profile release --runner "cafrun -n 2"
```
With `gfortran` 13 or earlier versions and OpenCoarrays installed,
```
fpm test --compiler caf --profile release --runner "cafrun -n 2" --flag "-ffree-line-length-0"
```
### Intel `ifx`
#### Single-image (serial) execution
```
fpm test --compiler ifx --profile release
```
#### Multi-image (parallel) execution
With Intel Fortran and Intel MPI installed,
```
fpm test --compiler ifx --profile release --flag "-coarray -DASSERT_MULTI_IMAGE"
```
### LLVM `flang-new`
#### Single-image (serial) execution
With `flang-new` version 19, use
```
fpm test --compiler flang-new --flag "-mmlir -allow-assumed-rank -O3"
```
With `flang-new` version 20 or later, use
```
fpm test --compiler flang-new --flag "-O3"
```
### LFortran `lfortran`
#### Single-image (serial) execution
```
fpm test --compiler lfortran --profile release --flag --cpp
```
### Numerical Algorithms Group (NAG) `nagfor`
#### Single-image (serial) execution
With `nagfor` version 7.1 or later, use
```
fpm test --compiler nagfor --flag -fpp
```
#### Multi-image execution
With `nagfor` 7.1, use
```
fpm test --compiler nagfor --profile release --flag "-fpp -coarray=cosmp -f2018"
```
With `nagfor` 7.2 or later, use
```
fpm test --compiler nagfor --flag -fpp
```
Documentation
-------------
Please see [example/README.md] and the [tests] for examples of how to use Assert.
### Potential pitfalls of `call_assert` macros:
The `call_assert*` macros from the `assert_macros.h` header file provide the
attractive guarantee that they will always compile *completely* away when
assertions are disabled, regardless of compiler analyses and optimization
level. This means users can reap the maintainability and correctness benefits
of aggressively asserting invariants throughout their code, without needing to
balance any potential performance cost associated with such assertions when the
code runs in production.
Unfortunately, preprocessor macros do not integrate cleanly with some aspects
of the Fortran language. As such, you might encounter one or more of the
following pitfalls when using these macros.
#### Line length limit
Up to and including the Fortran 2018 language standard, compilers were only
required to support up to 132 characters per free-form source line.
Preprocessor macro invocations are always expanded to a single line during
compilation, so when passing non-trivial arguments to macros including
`call_assert*` it becomes easy for the expansion to exceed this line length
limit. This can result in compile-time errors like the following from gfortran:
```
Error: Line truncated at (1) [-Werror=line-truncation]
```
Some compilers offer a command-line argument that can be used to workaround this legacy limit, e.g.,
* `gfortran -ffree-line-length-0` or equivalently `gfortran -ffree-line-length-none`
When using `fpm`, one can pass such a flag to the compiler using the `fpm --flag` option, e.g.,
```shell
$ fpm test --profile release --flag -ffree-line-length-0
```
Thankfully, Fortran 2023 raised this obsolescent line limit to 10,000
characters, so by using newer compilers you might never encounter this problem.
In the case of gfortran, this appears to have been resolved by default starting in release 14.1.0.
#### Line breaks in macro invocations
The preprocessor is not currently specified by any Fortran standard, and
as of 2025 its operation differs in subtle ways between compilers.
One way in which compilers differ is how macro invocations can safely be broken
across multiple lines.
For example, gfortran and flang-new both accept backslash `\` continuation
character for line-breaks in a macro invocation:
```fortran
! OK for flang-new and gfortran
call_assert_describe( computed_checksum == expected_checksum, \
"Checksum mismatch failure!" \
)
```
Whereas Cray Fortran wants `&` line continuation characters, even inside
a macro invocation:
```fortran
! OK for Cray Fortran
call_assert_describe( computed_checksum == expected_checksum, &
"Checksum mismatch failure!" &
)
```
There appears to be no syntax acceptable to all compilers, so when writing
portable code it's probably best to avoid line breaks inside a macro invocation.
#### Comments in macro invocations
Fortran does not support comments with an end delimiter,
only to-end-of-line comments. As such, there is no portable way to safely insert a
Fortran comment into the middle of a macro invocation. For example, the
following seemingly reasonable code results in a syntax error
after macro expansion (on gfortran and flang-new):
```fortran
! INCORRECT: cannot use Fortran comments inside macro invocation
call_assert_describe( computed_checksum == expected_checksum, ! ensured since version 3.14
"Checksum mismatch failure!" ! TODO: write a better message here
)
```
Depending on your compiler it *might* be possible to use a C-style block
comment (because they are often removed by the preprocessor), for example with
gfortran one can instead write the following:
```fortran
call_assert_describe( computed_checksum == expected_checksum, /* ensured since version 3.14 */ \
"Checksum mismatch failure!" /* TODO: write a better message here */ \
)
```
However that capability might not be portable to other Fortran compilers.
When in doubt, one can always move the comment outside the macro invocation:
```fortran
! assert a property ensured since version 3.14
call_assert_describe( computed_checksum == expected_checksum, \
"Checksum mismatch failure!" \
) ! TODO: write a better message above
```
Clients of Assert
-----------------
A few packages that use Assert include
* The [Julienne](https://go.lbl.gov/julienne) correctness-checking framework wraps Assert and defines idioms that automatically generate diagnostic messages containing program data.
* The [Caffeine](https://go.lbl.gov/caffeine) multi-image Fortran compiler runtime library uses Assert for internal sanity checks and interface validation.
* The [Fiats](https://go.lbl.gov/fiats) deep learning library uses Assert and Julienne.
* The [Matcha](https://go.lbl.gov/matcha) T-cell motility simulator also uses Assert and Julienne.
Legal Information
-----------------
See the [LICENSE](LICENSE) file for copyright and licensing information.
[Hyperlinks]:#
[OpenCoarrays]: https://github.com/sourceryinstitute/opencoarrays
[Enforcing programming contracts]: #enforcing-programming-contracts
[Single-image execution]: #single-image-execution
[example/README.md]: ./example/README.md
[tests]: ./tests
[Fortran Package Manager]: https://github.com/fortran-lang/fpm
[OCL]: https://en.wikipedia.org/wiki/Object_Constraint_Language
[example/invoke-via-macro.F90]: ./example/invoke-via-macro.F90
[Producing output in pure procedures]: #producing-output-in-pure-procedures
[Julienne]: https://go.lbl.gov/julienne
|