https://github.com/berkeleylab/julienne
A Fortran 2023 correctness-checking framework supporting expressive idioms for writing assertions and tests
Science Score: 26.0%
This score indicates how likely this project is to be science-related based on various indicators:
-
○CITATION.cff file
-
✓codemeta.json file
Found codemeta.json file -
✓.zenodo.json file
Found .zenodo.json file -
○DOI references
-
○Academic publication links
-
○Academic email domains
-
○Institutional organization owner
-
○JOSS paper metadata
-
○Scientific vocabulary similarity
Low similarity (8.7%) to scientific vocabulary
Repository
A Fortran 2023 correctness-checking framework supporting expressive idioms for writing assertions and tests
Basic Info
Statistics
- Stars: 15
- Watchers: 1
- Forks: 3
- Open Issues: 7
- Releases: 29
Metadata Files
README.md
Julienne: Idiomatic Correctness Checking for Fortran 2023
The Julienne framework offers a unified approach to unit testing and runtime assertion checking. Julienne defines idioms for specifying correctness conditions in a common way when writing tests that wrap the tested procedures or assertions that conditionally execute inside procedures to check correctness. Julienne's idioms center around expressions built from defined operations: a uniquely flexible Fortran capability allowing developers to define new operators in addition to overloading Fortran's intrinsic operators. The following table provides examples of the expressions Julienne supports:
Example expressions | Operand types
--------------------------------------------------|-----------------------------------------------------------
x .approximates. y .within. tolerance | real, double precision
x .approximates. y .withinFraction. tolerance | real, double precision
x .approximates. y .withinPercentage. tolerance | real, double precision
.all. ([i,j] .lessThan. k) | integer, real, double precision
.all. ([i,j] .lessThan. [k,m]) | integer, real, double precision
.all. (i .lessThan. [k,m]) | integer, real, double precision
(i .lessThan. j) .also. (k .equalsExpected. m)) | integer, real, double precision
x .lessThan. y | integer, real, double precision
x .greaterThan. y | integer, real, double precision
i .equalsExpected. j | integer, character, type(c_ptr)
i .isAtLeast. j | integer, real, double precision
i .isAtMost. j | integer, real, double precision
s .isBefore. t | character
s .isAfter. t | character
.expect. command_line%argument_present("--help")| logical
where
* .isAtLeast. and .isAtMost. can alternatively be spelled .greaterThanOrEqualTo. and .lessThanOrEqualTo., respectively;
* .equalsExpected. uses ==, which implies that trailing blank spaces are ignored in character operands;
* .equalsExpected. with integer operands supports default integers and integer(c_size_t);
* .isBefore. and .isAfter. verify alphabetical and reverse-alphabetical order, respectively; and
* .all. aggregates arrays of expression results, reports a consensus result, passes, and shows diagnostics only for failing tests, if any;
* .equalsExpected. generates asymmetric diagnostic output for failures, denoting the left- and right-hand sides as the actual value and expected values, respectively; and.
* appending a trailing string to an idiom with operator(//) appends the string to the resulting diagnostics string, if any.
Expressive idioms
Assertions
Any of the above expressions can be the actual argument in an invocation
of Julienne's call_julienne_assert function-line preprocessor macro:
fortran
call_julienne_assert(x .lessThan. y)
which a preprocessor will replace with a call to Julienne's assertion subroutine
when compiling with -DASSERTIONS. Otherwise, the preprocessor will remove the
above line entirely when -DASSERTIONS is not present.
Unit tests
The above tabulated expressions can also serve as function results in unit tests.
Constraints
All operands in an expression must be compatible in type and kind as well as
conformable in rank, where the latter condition implies that the operands must
be all scalars or all arrays with the same shape or a combination of scalars and
arrays with the same shape. This constraint follows from each of the binary
operators being elemental. The unary .all. operator applies to operands of
any rank.
Each tabulated expression above produces a test_diagnosis_t object with two
components:
- a
logicalindicator of test success if.true. or failure if.false.and - an automated diagnostic messages generated only if the test or assertion fails.
Custom Test Diagnostics
For cases in which Julienne's operators do not support the desired correctness
condition, the framework provides string-handling utilities for use in crafting
custom diagnostic messages. The string utilities center around Julienne's
string_t derived type, which offers elemental constructor functions, i.e.,
functions that one invokes via the same name as the derived type: string_t().
The string_t() constructor functions convert data of numeric type to
character type, storing the resulting character representation in a private
component of the constructor function result. The actual argument provided to
the constructor function can be of any one of several types, kinds, and ranks.
Julienne provides defined operations for concatenating string_t objects
(//), forming a concatenated string_t object from an array of string_t
objects (.cat.), forming a separated-value list (.separatedBy. or
equivalently .sv.), including a comma-separated value list (.csv.). The
table below shows some expressions that Julienne supports with these defined
operations.
Example expression | Result
-------------------------------------------------|------------------------------------------------
s%bracket(), where s=string_t("abc"), | string_t("[abc]")
s%bracket("_"), where s=string_t("abc") | string_t("_abc_")
s%bracket("{","}"), where s=string_t("abc") | string_t("{abc}")
string_t(["a", "b", "c"]) | [string_t("a"), string_t("b"), string_t("c")]
.cat. string_t([9,8,7]) | string_t("987")
.csv. string_t([1.5,2.0,3.25]) | string_t("1.50000000,2.00000000,3.25000000")
"-" .separatedBy. string_t([1,2,4]) | string_t("1-2-4")
string_t("ab") // string_t("cd") | string_t("abcd")
"ab" // string_t("cd") | string_t("abcd")
string_t("ab") // "cd" | string_t("abcd")
One can use such expressions to craft a diagnostic message when constructing
a custom test function result:
fortran
type(test_diagnosis_t) test_diagnosis
test_diagnosis = test_diagnosis_t( &
test_passed = i==j, &
diagnostics_string = "expected " // string_t(i) // "; actual " //string_t(j) &
)
A file abstraction
Arrays of string_t objects provide a convenient way to store a ragged-length
array of character data. Julienne's file_t derived type has a private
component that is a string_t array, wherein each element is one line of a text
file. By storing a file in a file_t object using the file_t derived type's
constructor function one can confine a program's file input/output (I/O) to one
or two procedures. The resulting file_t object can be manipulated elsewhere
without incurring the costs associated with file I/O. For example, the following
line reads a file named data.txt into a file_t object and associates the name
file with the resulting object.
fortran
type(file_t) file
associate(file => file_t("data.txt"))
end associate
This style supports functional programming patterns in two ways. First, the rest
of the program can be comprised of pure procedures, which are precluded from
performing I/O. Second, an associate name is immutable when associated with an
expression, including an expression that is simply a function reference.
Functional programming revolves around creating and using immutable state.
(By contrast, when associating a name with a variable or array instead of with
an expression, only certain attributes, such as the entity's allocation status,
are immutable. The value of such a variable or array can be redefined.)
Functional Programming
Functional programming patterns centered around pure procedures enhance
code clarity, ease refactoring, and encourage optimization. For example,
the constraints on pure procedures make it easier for a developer or a
compiler to safely reorder program statements. Moreover, Fortran allows
invoking only pure procedures inside do concurrent, a construct that
compilers can automatically offload to a graphics processing unit (GPU).
Julienne lowers a widely stated barrier to writing pure procedures (including
simple procedures): the difficulty in printing values while debugging code.
The Julienne philosophy is that printing a value for debugging purposes implies
an expectation about the value. Assert such expectations by writing Julienne
expressions inspired by natural language. A program will proceed quietly past
a correct assertion. An incorrect assertion produces either automated or custom
diagnostic messages during error termination.
Getting Started
Writing Unit Tests
Please see demo/README.md for a detailed demonstration of test setup.
Writing Assertions
To write a Julienne assertion, insert a function-like preprocessor macro
call_julienne_assert on a single line as in each of the two macro
invocations below:
```fortran
include "julienne-assertion-macros.h"
program main
use, juliennem, only : calljulienneassert
implicit none
real, parameter :: x=1., y=2., tolerance=3.
calljulienneassert(x .approximates. y .within. tolerance)
calljulienneassert(abs(x-y) < tolerance)
end program
where inserting `-DASSERTIONS` in a compile command will expand the macros to
fortran
call calljulienneassert(x .approximates. y .within. tolerance, _FILE, __LINE)
call calljulienneassert(allocated(a), _FILE, __LINE)
``
and where dots (.) delimit Julienne operators. The above expression containing
Julienne operators evaluates to a Juliennetestdiagnosistobject, whereas
expressionallocated(a)on the subsequent line evaluates to alogicalvalue.
If an assertion containing a Julienne expression fails, Julienne inserts diagnostic
information into the stop code in an ultimateerror stop. If an expression
evaluates to alogicalvalue offalse., the error stop code will contain a
literal copy of the expression (e.g.,allocated(a)). In either case, Julienne
also inserts the file and line number into the stop code using via theFILE
andLINEmacros, respectively. Most compilers write the resulting stop code
toerror_unit`.
An Origin Story
Julienne's name derives from the term for vegetables sliced into thin strings: julienned vegetables. The Veggies and Garden unit-testing frameworks inspired the structure of Julienne's tests and output. Initially developed in the Sourcery repository as lightweight alternative with greater portability across compilers, Julienne's chief innovation now lies in the expressive idioms the framework supports.
Building and Testing
Compiler support
When built with the compiler versions tabulated below, all Julienne tests pass.
Compiler | Version(s) Tested | Known Issues
-----------------|------------------------|-------------
LLVM flang-new | 19, 20 | none
NAG nagfor | 7.2 Build 7227 | none
Intel ifx | 2025.2.1 | none
GCC gfortran | 13.4.0, 14.3.0, 15.1.0 | see below
With gfortran 13 through 14.2.0,
- The test_description_t constructor's diagnosis_function actual argument
must be a procedure pointer declared with procedure(diagnosis_function_i).
- The string_t type-bound function bracket crashes.
Build/test commands
LLVM (flang-new) compiler
flang-new version 20 or later
fpm test --compiler flang-new
flang-new version 19
Add the following command before the fpm command recommended above for
LLVM 20 or later:
bash
export FPM_FFLAGS="-mmlir -allow-assumed-rank"
where this FPM_FFLAGS setting turns on the support for Fortran's assumed-rank
dummy arguments.
If you do not have access to LLVM 19 or 20, we recommend building the llvm-project main branch from source. A script that might help with doing so is in the handy-dandy repository.
NAG (nagfor) compiler
fpm test --compiler nagfor --flag -fpp
GNU (gfortran) compiler
gfortran versions 14 or higher
fpm test --compiler gfortran --profile release
gfortran version 13
fpm test --compiler gfortran --profile release --flag "-ffree-line-length-none"
where the -ffree-line-length-none turns on support for lines exceeding the Fortran 2018 limit of 132 columns.
(Fortran 2023 expands the allowable line length to 5,000 characters.)
Intel (ifx) compiler
fpm test --compiler ifx --flag "-fpp -O3" --profile release
Older versions of ifx might require adding -coarray to the quoted argument just after --flag above.
Documentation
See our online documentation or build the documentation locally by installing FORD and executing ford ford.md.
Known Software Using Julienne
Owner
- Name: Berkeley Lab
- Login: BerkeleyLab
- Kind: organization
- Location: Berkeley, CA
- Website: https://www.lbl.gov
- Twitter: BerkeleyLab
- Repositories: 11
- Profile: https://github.com/BerkeleyLab
Lawrence Berkeley National Laboratory
GitHub Events
Total
- Create event: 95
- Issues event: 22
- Release event: 21
- Watch event: 9
- Delete event: 58
- Issue comment event: 16
- Push event: 259
- Pull request review comment event: 33
- Pull request review event: 44
- Pull request event: 109
- Fork event: 3
Last Year
- Create event: 95
- Issues event: 22
- Release event: 21
- Watch event: 9
- Delete event: 58
- Issue comment event: 16
- Push event: 259
- Pull request review comment event: 33
- Pull request review event: 44
- Pull request event: 109
- Fork event: 3
Issues and Pull Requests
Last synced: 10 months ago
All Time
- Total issues: 17
- Total pull requests: 70
- Average time to close issues: 23 days
- Average time to close pull requests: 2 days
- Total issue authors: 3
- Total pull request authors: 2
- Average comments per issue: 0.24
- Average comments per pull request: 0.07
- Merged pull requests: 53
- Bot issues: 0
- Bot pull requests: 0
Past Year
- Issues: 17
- Pull requests: 54
- Average time to close issues: 23 days
- Average time to close pull requests: 3 days
- Issue authors: 3
- Pull request authors: 2
- Average comments per issue: 0.24
- Average comments per pull request: 0.09
- Merged pull requests: 38
- Bot issues: 0
- Bot pull requests: 0
Top Authors
Issue Authors
- rouson (10)
- ktras (4)
- bonachea (3)
Pull Request Authors
- rouson (65)
- ktras (5)