Skip to content

Commit

Permalink
Merge pull request #41 from bonachea/assertions-knob
Browse files Browse the repository at this point in the history
Change assertion enforcement control knob to ASSERTIONS=1/0
  • Loading branch information
rouson authored Oct 4, 2024
2 parents fc867ce + 22080ee commit 6047dd8
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
- name: Build Developer Documentation
run: |
ford -I include doc-generator.md > ford_output.txt
# Turn warnings into errors
ford doc-generator.md > ford_output.txt
cat ford_output.txt; if grep -q -i Warning ford_output.txt; then exit 1; fi
cp ./README.md ./doc/html
Expand Down
54 changes: 36 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
Assert
======

A simple assertion utility taking advantage of the Fortran 2018 standard's introduction of variable stop codes
and error termination inside pure procedures.
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
-----------
Expand All @@ -11,18 +10,37 @@ Motivations

Overview
--------
This assertion utility contains three public entities:
This assertion utility contains four public entities:

1. An `assert` subroutine,
2. A `characterizable_t` abstract type supporting `assert`, and
3. An `intrinsic_array_t` non-abstract type extending `characterizable_t`.
4. An `assert_macros.h` header file containing C-preprocessor macros.

The `assert` subroutine

* Error-terminates with a variable stop code when a user-defined logical assertion fails,
* Error-terminates with a variable stop code when a caller-provided logical assertion fails,
* Includes user-supplied diagnostic data in the output if provided by the calling procedure,
* Is callable inside `pure` procedures, and
* Can be eliminated during an optimizing compiler's dead-code removal phase based on a preprocessor macro: `-DUSE_ASSERTIONS=.false.`.
* Can be eliminated at compile-time, as controlled by the `ASSERTIONS` preprocessor define.

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, eg:
```
fpm build --flag "-DASSERTIONS"
```
The program [example/invoke-via-macro.F90] demonstrates the preferred way to invoke the `assert` subroutine via the three provided macros.
Invoking `assert` this way insures that `assert` invocations 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.

The `characterizable_t` type defines an `as_character()` deferred binding that produces `character` strings for use as diagnostic output from a user-defined derived type that extends `characterizable_t` and implements the deferred binding.

Expand All @@ -43,7 +61,7 @@ The requirements and assurances might be constraints of three kinds:
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 [examples/README.md] file shows examples of writing constraints in notes on class diagrams using the formal syntax of the Object Constraint Language ([OCL]).
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]).

Downloading, Building, and Running Examples
-------------------------------------------
Expand All @@ -58,14 +76,14 @@ cd assert
#### Single-image (serial) execution
The following command builds Assert and runs the full test suite in a single image:
```
fpm test --profile release
fpm test --profile release --flag "-ffree-line-length-0"
```
which builds the Assert library and runs the test suite.
which builds the Assert library (with the default of assertion enforcement disabled) and runs the test suite.

#### Multi-image (parallel) execution
With `gfortran` and OpenCoarrays installed,
```
fpm test --compiler caf --profile release --runner "cafrun -n 2"
fpm test --compiler caf --profile release --runner "cafrun -n 2" --flag "-ffree-line-length-0"
```
To build and test with the Numerical Algorithms Group (NAG) Fortran compiler version
7.1 or later, use
Expand All @@ -80,7 +98,6 @@ fpm test --compiler ifx --profile release --flag -coarray
### Building and testing with the LLVM `flang-new` compiler
```
fpm test --compiler flang-new --flag "-mmlir -allow-assumed-rank -O3"
```

### Building and testing with the Numerical Algorithms Group (NAG) compiler
Expand Down Expand Up @@ -180,9 +197,9 @@ Instead when breaking long lines in a macro invocation, just break the line (no
continuation character!), eg:

```fortran
! When breaking a lines in a macro invocation, just use new-line with no `&` continuation character:
call_assert_diagnose( computed_checksum == expected_checksum,
"Checksum mismatch failure!",
! When breaking a line in a macro invocation, use backslash `\` continuation character:
call_assert_diagnose( computed_checksum == expected_checksum, \
"Checksum mismatch failure!", \
expected_checksum )
```

Expand All @@ -206,8 +223,8 @@ comment (because they are removed by the preprocessor), for example with
gfortran one can instead write the following:

```fortran
call_assert_diagnose( computed_checksum == expected_checksum, /* ensured since version 3.14 */
"Checksum mismatch failure!", /* TODO: write a better message here */
call_assert_diagnose( computed_checksum == expected_checksum, /* ensured since version 3.14 */ \
"Checksum mismatch failure!", /* TODO: write a better message here */ \
computed_checksum )
```

Expand All @@ -216,8 +233,8 @@ When in doubt, one can always move the comment outside the macro invocation:

```fortran
! assert a property ensured since version 3.14
call_assert_diagnose( computed_checksum == expected_checksum,
"Checksum mismatch failure!",
call_assert_diagnose( computed_checksum == expected_checksum, \
"Checksum mismatch failure!", \
computed_checksum ) ! TODO: write a better message above
```

Expand All @@ -237,3 +254,4 @@ See the [LICENSE](LICENSE) file for copyright and licensing information.
[OCL]: https://en.wikipedia.org/wiki/Object_Constraint_Language
[Assert's GitHub Pages site]: https://berkeleylab.github.io/assert/
[`ford`]: https://github.com/Fortran-FOSS-Programmers/ford
[example/invoke-via-macro.F90]: ./example/invoke-via-macro.F90
16 changes: 12 additions & 4 deletions example/invoke-via-macro.F90
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@

program invoke_via_macro
!! Demonstrate how to invoke the 'assert' subroutine using a preprocessor macro that facilitates
!! the complete removal of the call in the absence of the compiler flag -DDEBUG.
!! the complete removal of the call in the absence of the compiler flag: -DASSERTIONS
use assert_m, only : assert, intrinsic_array_t, string
!! If an "only" clause is employed as above, it must include the "string" function that the
!! call_assert* macros reference when transforming the code below into "assert" subroutine calls.
implicit none

#ifndef DEBUG
#if !ASSERTIONS
print *
print *,'To enable the "assert" call, define -DDEBUG, e.g., fpm run --example invoke-via-macro --flag "-DDEBUG -fcoarray=single"'
print *,'To enable the "call_assert" invocations, define the ASSERTIONS macro. e.g.:'
print *,' fpm run --example invoke-via-macro --flag "-DASSERTIONS -fcoarray=single -ffree-line-length-0"'
print *
#endif

! The C preprocessor will convert each call_assert* macro below into calls to the "assert" subroutine
! (if -DDEBUG is in the compiler command) or into nothing (if -DDEBUG is not in the compiler command).
! whenever the ASSERTIONS macro is defined to non-zero (e.g. via the -DASSERTIONS compiler flag).
! Whenever the ASSERTIONS macro is undefined or defined to zero (e.g. via the -DASSERTIONS=0 compiler flag),
! these calls will be entirely removed by the preprocessor.

call_assert(1==1) ! true assertion
call_assert_describe(2>0, "example assertion invocation via macro") ! true assertion
call_assert_diagnose(1+1==2, "example with scalar diagnostic data", 1+1) ! true assertion
#if ASSERTIONS
print *
print *,'Here comes the expected assertion failure:'
print *
#endif
call_assert_diagnose(1+1>2, "example with array diagnostic data" , intrinsic_array_t([1,1,2])) ! false assertion

end program invoke_via_macro
7 changes: 6 additions & 1 deletion include/assert_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
#undef call_assert_describe
#undef call_assert_diagnose

#ifdef DEBUG
#ifndef ASSERTIONS
! Assertions are off by default
#define ASSERTIONS 0
#endif

#if ASSERTIONS
# define call_assert(assertion) call assert(assertion, "No description provided (see file " // __FILE__ // ", line " // string(__LINE__) // ")")
# define call_assert_describe(assertion, description) call assert(assertion, description // " in file " // __FILE__ // ", line " // string(__LINE__) // ": " )
# define call_assert_diagnose(assertion, description, diagnostic_data) call assert(assertion, "file " // __FILE__ // ", line " // string(__LINE__) // ": " // description, diagnostic_data)
Expand Down
31 changes: 23 additions & 8 deletions src/assert/assert_subroutine_m.F90
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
! (c) 2024 UC Regents, see LICENSE file for detailed terms.
!
! (c) 2019-2020 Guide Star Engineering, LLC
! This Software was developed for the US Nuclear Regulatory Commission (US NRC) under contract
! "Multi-Dimensional Physics Implementation into Fuel Analysis under Steady-state and Transients (FAST)",
! contract # NRC-HQ-60-17-C-0007
!
#include "assert_macros.h"

module assert_subroutine_m
!! summary: Utility for runtime checking of logical assertions.
!! summary: Utility for runtime enforcement of logical assertions.
!! usage: error-terminate if the assertion fails:
!!
!! use assertions_m, only : assert
!! call assert( 2 > 1, "2 > 1")
!!
!! Turn off assertions in production code by setting USE_ASSERTIONS to .false. via the preprocessor.
!! 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 runtime 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.
!!
!! Disabling assertion enforcement may eliminate any associated runtime
!! overhead by enabling optimizing compilers to ignore the assertion procedure
!! body during a dead-code-removal phase of optimization.
!!
!! To enable assertion enforcement (e.g., for a debug build), define the preprocessor ASSERTIONS to non-zero.
!! This file's capitalized .F90 extension causes most Fortran compilers to preprocess this file so
!! that building as follows turns off assertion enforcement:
!! that building as follows enables assertion enforcement:
!!
!! fpm build --flag "-DUSE_ASSERTIONS=.false."
!! fpm build --flag "-DASSERTIONS"
!!
!! Doing so may eliminate any associated runtime overhead by enabling optimizing compilers to ignore
!! the assertion procedure body during a dead-code-removal phase of optimization.
implicit none
private
public :: assert

#ifndef USE_ASSERTIONS
# define USE_ASSERTIONS .true.
# if ASSERTIONS
# define USE_ASSERTIONS .true.
# else
# define USE_ASSERTIONS .false.
# endif
#endif
logical, parameter :: enforce_assertions=USE_ASSERTIONS
!! Turn off assertions as follows: fpm build --flag "-DUSE_ASSERTIONS=.false."

interface

Expand Down
31 changes: 17 additions & 14 deletions test/test-assert-macro.F90
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,60 @@ program test_assert_macros
print *
print *,"The call_assert macro"

#define DEBUG
#undef ASSERTIONS
#define ASSERTIONS 1
#include "assert_macros.h"
call_assert(1==1)
print *," passes on not error-terminating when an assertion expression evaluating to .true. is the only argument"

#undef DEBUG
#undef ASSERTIONS
#include "assert_macros.h"
call_assert(.false.)
print *," passes on being removed by the preprocessor when DEBUG is undefined" // new_line('')
print *," passes on being removed by the preprocessor when ASSERTIONS is undefined" // new_line('')

!------------------------------------------

print *,"The call_assert_describe macro"

#define DEBUG
#undef ASSERTIONS
#define ASSERTIONS 1
#include "assert_macros.h"
call_assert_describe(.true., ".true.")
print *," passes on not error-terminating when assertion = .true. and a description is present"

#undef DEBUG
#undef ASSERTIONS
#include "assert_macros.h"
call_assert_describe(.false., "")
print *," passes on being removed by the preprocessor when DEBUG is undefined" // new_line('')
print *," passes on being removed by the preprocessor when ASSERTIONS is undefined" // new_line('')

!------------------------------------------

print *,"The call_assert_diagnose macro"

#define DEBUG
#undef ASSERTIONS
#define ASSERTIONS 1
#include "assert_macros.h"
call_assert_diagnose(.true., ".true.", diagnostic_data=1)
print *," passes on not error-terminating when assertion = .true. and description and diagnostic_data are present"

block
integer :: computed_checksum = 37, expected_checksum = 37

call_assert_diagnose( computed_checksum == expected_checksum,
"Checksum mismatch failure!",
expected_checksum )
call_assert_diagnose( computed_checksum == expected_checksum, \
"Checksum mismatch failure!", \
expected_checksum )
print *," passes with macro-style line breaks"

call_assert_diagnose( computed_checksum == expected_checksum, /* ensured since version 3.14 */
"Checksum mismatch failure!", /* TODO: write a better message here */
call_assert_diagnose( computed_checksum == expected_checksum, /* ensured since version 3.14 */ \
"Checksum mismatch failure!", /* TODO: write a better message here */ \
computed_checksum )
print *," passes with C block comments embedded in macro"

end block

#undef DEBUG
#undef ASSERTIONS
#include "assert_macros.h"
call_assert_describe(.false., "")
print *," passes on being removed by the preprocessor when DEBUG is undefined"
print *," passes on being removed by the preprocessor when ASSERTIONS is undefined"

end program

0 comments on commit 6047dd8

Please sign in to comment.