diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index e63bd4e..1806a06 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -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 diff --git a/README.md b/README.md index 55f7721..e39ba10 100644 --- a/README.md +++ b/README.md @@ -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 ----------- @@ -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. @@ -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 ------------------------------------------- @@ -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 @@ -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 @@ -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 ) ``` @@ -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 ) ``` @@ -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 ``` @@ -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 diff --git a/example/invoke-via-macro.F90 b/example/invoke-via-macro.F90 index 3799990..b89df91 100644 --- a/example/invoke-via-macro.F90 +++ b/example/invoke-via-macro.F90 @@ -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 diff --git a/include/assert_macros.h b/include/assert_macros.h index 065f8d1..c1b4872 100644 --- a/include/assert_macros.h +++ b/include/assert_macros.h @@ -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) diff --git a/src/assert/assert_subroutine_m.F90 b/src/assert/assert_subroutine_m.F90 index 90e0e11..7ac135c 100644 --- a/src/assert/assert_subroutine_m.F90 +++ b/src/assert/assert_subroutine_m.F90 @@ -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 diff --git a/test/test-assert-macro.F90 b/test/test-assert-macro.F90 index 770df48..7384141 100644 --- a/test/test-assert-macro.F90 +++ b/test/test-assert-macro.F90 @@ -5,35 +5,38 @@ 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" @@ -41,21 +44,21 @@ program test_assert_macros 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