Introduction — Fypp 3.2 documentation (2024)

Fypp is a Python powered preprocessor. It can be used for any programminglanguages but its primary aim is to offer a Fortran preprocessor, which helps toextend Fortran with condititional compiling and template metaprogrammingcapabilities. Instead of introducing its own expression syntax, it uses Pythonexpressions in its preprocessor directives, offering the consistency andversatility of Python when formulating metaprogramming tasks. It puts strongemphasis on robustness and on neat integration into developing toolchains.

Fypp was inspired by the pyratemp templating engine[1]. Although it shares many concepts with pyratemp, it was written fromscratch focusing on the special needs when preprocessing source code. Fyppnatively supports the output of line numbering markers, which are used bymany compilers to generate compiler messages with correct line numbers. Unlikemost cpp/fpp-like preprocessors or the coco preprocessor, Fypp also supportsiterations, multiline macros, continuation lines in preprocessor directives andautomatic line folding. It generally tries to extend the modern Fortran languagewith metaprogramming capabilities without tempting you to use it for tasks whichcould/should be done in Fortran itself.

The project is hosted on github withdocumentation available on readthedocs.org. Fypp is released under the BSD 2-clauselicense.

This document describes Fypp Version 3.2.

Features

Below you find a summary over Fypps main features. Each of them is describedmore in detail in the individual sections further down.

  • Definition, evaluation and removal of variables:

    #:if DEBUG > 0 print *, "Some debug information"#:endif#:set LOGLEVEL = 2print *, "LOGLEVEL: ${LOGLEVEL}$"#:del LOGLEVEL
  • Macro definitions and macro calls:

    #:def ASSERT(cond) #:if DEBUG > 0 if (.not. ${cond}$) then print *, "Assert failed in file ${_FILE_}$, line ${_LINE_}$" error stop end if #:endif#:enddef ASSERT! Invoked via direct call (argument needs no quotation)@:ASSERT(size(myArray) > 0)! Invoked as Python expression (argument needs quotation)$:ASSERT('size(myArray) > 0')
  • Conditional output:

    program test#:if defined('WITH_MPI') use mpi#:elif defined('WITH_OPENMP') use openmp#:else use serial#:endif
  • Iterated output (e.g. for generating Fortran templates):

    interface myfunc#:for dtype in ['real', 'dreal', 'complex', 'dcomplex'] module procedure myfunc_${dtype}$#:endforend interface myfunc
  • Inline directives:

    logical, parameter :: hasMpi = #{if defined('MPI')}# .true. #{else}# .false. #{endif}#
  • Insertion of arbitrary Python expressions:

    character(*), parameter :: comp_date = "${time.strftime('%Y-%m-%d')}$"
  • Inclusion of files during preprocessing:

    #:include "macrodefs.fypp"
  • Using Fortran-style continutation lines in preprocessor directives:

    #:if var1 > var2 & & or var2 > var4 print *, "Doing something here"#:endif
  • Passing (unquoted) multiline string arguments to callables:

    #! Callable needs only string argument#:def DEBUG_CODE(code) #:if DEBUG > 0 $:code #:endif#:enddef DEBUG_CODE#! Pass code block as first positional argument#:block DEBUG_CODE if (size(array) > 100) then print *, "DEBUG: spuriously large array" end if#:endblock DEBUG_CODE#! Callable needs also non-string argument types#:def REPEAT_CODE(code, repeat) #:for ind in range(repeat) $:code #:endfor#:enddef REPEAT_CODE#! Pass code block as positional argument and 3 as keyword argument "repeat"#:block REPEAT_CODE(repeat=3)this will be repeated 3 times#:endblock REPEAT_CODE
  • Preprocessor comments:

    #! This will not show up in the output#! Also the newline characters at the end of the lines will be suppressed
  • Suppressing the preprocessor output in selected regions:

    #! Definitions are read, but no output (e.g. newlines) will be produced#:mute#:include "macrodefs.fypp"#:endmute
  • Explicit request for stopping the preprocessor:

    #:if DEBUGLEVEL < 0 #:stop 'Negative debug level not allowed!'#:endif
  • Easy check for macro parameter sanity:

    #:def mymacro(RANK) #! Macro only works for RANK 1 and above #:assert RANK > 0 :#:enddef mymacro
  • Line numbering markers in output:

    program test#:if defined('MPI')use mpi#:endif:

    transformed to

    # 1 "test.fypp" 1program test# 3 "test.fypp"use mpi# 5 "test.fypp":

    when variable MPI is defined and Fypp was instructed to generate linemarkers.

  • Automatic folding of generated lines exceeding line length limit

Installing

Fypp needs a working Python 3 interpreter (Python 3.5 or above).

When you install Fypp, you obtain the command line tool fypp and the Pythonmodule fypp.py. Latter you can import if you want to access thefunctionality of Fypp directly from within your Python scripts.

Installing via conda

The last stable release of Fypp can be easily installed as conda package byissuing

conda install -c conda-forge fypp

Installing via pip

You can also use Pythons command line installer pip in order to download thestable release from the Fypp page on PyPIand install it on your system.

If you want to install Fypp into the module system of the active Python 3interpreter (typically the case when you are using a Python virtualenvironment), issue

pip3 install fypp

Alternatively, you can install Fypp into the user space (under ~/.local) with

pip3 install --user fypp

Manual install

For a manual install, you can download the source code of the latest stablerelease from the Fypp project website.

If you wish to obtain the latest development version, clone the projectsrepository:

git clone https://github.com/aradi/fypp.git

and check out the master branch.

The command line tool is a single stand-alone script. You can run it directlyfrom the source folder

FYPP_SOURCE_FOLDER/bin/fypp

or after copying it from the bin folder to any location listed in your PATHenvironment variable, by just issuing

fypp

The python module fypp.py can be found in FYP_SOURCE_FOLDER/src.

Testing

Simple manual testing can be done by issuing the command

./test/runtests.sh

from the root of the Fypp source tree. This executes the unit tests shipped withFypp with the default Python interpreter in your path. If you wish to use aspecific interpreter, you can pass it as argument to the script:

./test/runtests.sh python3

You can also pass multiple interpreters as separate arguments. In that casethe testing will be carried out for each of them.

Testing for developers

If you wish to contribute to Fypp, you should have tox installed on yoursystem, so that you can test the packaged project in isolated environmentsbefore issuing a pull request.

In order to execute the unit tests with tox, run

tox

from the root folder of the source tree. This tries to test Fypp with variousdifferent python interpreters. If you want to limit testing to selectedinterpeters only, select the environment with the appropriate command lineswitch, e.g.

tox -e py34

Running

The Fypp command line tool reads a file, preprocesses it and writes it toanother file, so you would typically invoke it like:

fypp source.fpp source.f90

which would process source.fpp and write the result to source.f90. Ifinput and output files are not specified, information is read from stdin andwritten to stdout.

The behavior of Fypp can be influenced with various command line options. Asummary of all command line options can be obtained by:

fypp -h

General syntax

Fypp has three types of preprocessor directives, all of them having a line andan inline form:

  • Control directives

    • Line form, starting with #: (hashmark colon):

      #:if 1 > 2 Some code#:endif
    • Inline form, enclosed between #{ and }#:

      #{if 1 > 2}#Some code#{endif}#
  • Eval directives

    • Line form, starting with $: (dollar colon):

      $:time.strftime('%Y-%m-%d')
    • Inline form, enclosed between ${ and }$:

      print *, "Compilation date: ${time.strftime('%Y-%m-%d')}$"
  • Direct call directive

    • Line form, starting with @: (at colon):

      @:mymacro(a < b)
    • Inline form, enclosed between @{ and }@:

      print *, @{mymacro(a < b)}@

The line form must always start at the beginning of a line (preceded by optionalwhitespace characters only) and it ends at the end of the line. The inline formcan appear anywhere, but if the construct consists of several directives(e.g. #{if ...}# and #{endif}#), all of them must appear on the sameline. While both forms can be used at the same time, they must be consistent fora particular construct, e.g. a directive opened as line directive can not beclosed by an inline directive and vica versa.

Whitespaces in preprocessor commands are ignored if they appear after theopening colon or curly brace or before the closing curly brace. So the followingexamples are pairwise equivalent:

#:if 1 > 2#: if 1 > 2#{if 1 > 2}##{ if 1 > 2 }#$:time.strftime('%Y-%m-%d')$: time.strftime('%Y-%m-%d')${time.strftime('%Y-%m-%d')}$${ time.strftime('%Y-%m-%d') }$

Starting whitespaces before line directives are ignored, enabling you to chooseany indentation strategy you like for the directives:

program test : do ii = 1, nn print *, ii #:if DEBUG > 0 print *, "Some debug info about iteration ${ii}$" #:endif print *, "Normal code" end do :end program test

Preprocessor directives can be arbitrarily nested:

#:if DEBUG > 0 #:if DO_LOGGING ... #:endif#:endif

Every open directive must be closed before the end of the file is reached.

In all control directives, the whitespace separating the name of the directivefrom the following parameter is obligatory. Therefore, the following example issyntactically incorrect:

#! Incorrect due to missing whitespace after 'if'#:if(1 > 2)

Expression evaluation

Python expressions can occur either as part of control directives, like

#:if DEBUG > 0#:for dtype in ['real(dp)', 'integer', 'logical']

or directly inserted into the code using eval directives.

$:time.strftime('%Y-%m-%d')print *, "${time.strftime('%Y-%m-%d')}$"

Expressions are always evaluated by using Pythons eval() builtin and mustbe, therefore, syntactically and semantically correct Pythonexpressions. Although, this may require some additional quotations as comparedto other preprocessor languages

#:if defined('DEBUG') #! The Python function defined() expects a string argument#:for dtype in ['real(dp)', 'integer', 'logical'] #! dtype runs over strings

it enables consistent expressions with (hopefully) least surprises (once youknow, how to formulate the expression in Python, you exactly know, how to writeit for Fypp). Also, note, that variable names, macros etc. are for Python (andtherefore also for Fypp) case sensitive.

When you access a variable in an expression, it must have been already definedbefore, either via command line options or via preprocessor directives. Forexample the directive

#:if DEBUG > 0

can only be evaluated, if the variable DEBUG had been already defined before.

Python sandbox

Python expressions are evaluated in an isolated Python environment, whichcontains a restricted set of Python built-in functions and a few predefinedvariables and functions (see below). There are no modules loaded by default, andfor safety reasons, no modules can be loaded once the preprocessing hasstarted, but can be loaded at startup if needed.

Predefined variables

The isolated Python environment for the expression evaluation contains followingpredefined global variables:

  • _THIS_LINE_: number of current line

  • _THIS_FILE_: name of current file

  • _LINE_: number of current line in the processed input file

  • _FILE_: name of processed input file

    print *, "This is line nr. ${_LINE_}$ in file '${_FILE_}$'"
  • _DATE_: current date in ISO format

  • _TIME_: current time:

    print *, "Rendering started ${_DATE_}$ ${_TIME_}$"
  • _SYSTEM_: Name of the system Fypp runs on, as returned by Pythonsplatform.system() function (e.g. Linux, Windows, Darwin, etc.)

  • _MACHINE_: Name of the current machine Fypp runs on, as returned byPythons platform.machine() function (e.g. x86_64)

The predefined variables _FILE_ and _LINE_ differ from theircounterparts _THIS_FILE_ and _THIS_LINE_ only within macros. When amacro is executed, the variables _THIS_FILE_ and _THIS_LINE_ specify theposition, where the expression containing these variables is located, while thevariables _FILE_ and _LINE_ refer to the position in the processed file,from where the macro was called (and where the result of the evaluation will beinserted later). For example, the input

#:def macro()IN MACRO: _THIS_LINE_=${_THIS_LINE_}$, _LINE_=${_LINE_}$#:enddef macroGLOBAL: _THIS_LINE_=${_THIS_LINE_}$, _LINE_=${_LINE_}$ | ${macro()}$

yields after being processed by Fypp:

GLOBAL: _THIS_LINE_=5, _LINE_=5 | IN MACRO: _THIS_LINE_=2, _LINE_=5

If from within a macro an other macro is called, the variables _FILE_ and_LINE_ will keep their original values, while _THIS_FILE_ and_THIS_LINE_ will be continuously updated within the nested macro as well.

Predefined functions

Following predefined functions are available:

  • defined(VARNAME): Returns True if a variable with a given name hasbeen already defined. The variable name must be provided as string:

    #:if defined('WITH_MPI')
  • getvar(VARNAME, DEFAULTVALUE): Returns the value of a variable or adefault value if the variable is not defined. The variable name must beprovided as string:

    #:if getvar('DEBUG', 0)
  • setvar(VARNAME, VALUE): Sets a variable to given value. It is identical tothe set directive. The variable name expression has the same format as inthe #:set directive, but must be quoted:

    $:setvar('i', 12)print *, "VAR I: ${i}$"

    Multiple assignments may be specified as subsequent argument pairs:

    $:setvar('i', 1, 'j', 2)print *, "VAR I: ${i}$, VAR J: ${j}$"
  • delvar(VARNAME): Removes a variable or a macro definition from the localscope. It is identical to the del directive. The variable nameexpression must be provided as in the #:del directive, but must be quoted:

    $:delvar('i')

    Additional variable name expressions may be specified as subsequent arguments:

    $:delvar('i', 'j')
  • globalvar(VARNAME): Adds a given variable as global variable to thecurrent scope. It is identical to the global directive. The variable nameexpression must be provided as in the #:global directive, but must bequoted:

    $:globalvar('i')

    Multiple variable name expressions may be specified as subsequent arguments.

Initializing variables

Initial values for preprocessor variables can be set via the command line option(-D) at startup:

fypp -DDEBUG=0 -DWITH_MPI

The assigned value for a given variable is evaluated in Python. If no value isprovided, None is assigned.

Importing modules at startup

Warning

Modules imported at startup have access to the fullunrestricted Python environment and can execute any Python code. Importonly trustworthy modules!

If a Python module is required for the preprocessing, it can be imported beforethe preprocessing starts via the command line option (-m):

fypp -m time

The example above would allow to process the line:

character(*), parameter :: comp_date = "${time.strftime('%Y-%m-%d')}$"

If more than one module is needed, each of them can imported with an individual-m option:

fypp -m time -m math

When importing modules with the -m option, the module search path consistsof the current directory, the directories in the PYTHONPATH environmentvariable and the standard Python module paths. Further lookup paths can bespecified using the option -M:

fypp -M mymoddir1 -M mymoddir2 -m mymodule -m mymodule2

The module directories are looked up in the order they are specified beforesearching at the default locations. Modules are imported also in the order oftheir specification at the command line.

Each module imported at startup has its own name space. Entities in the importedmodules can be accessed during the preprocessing in the usual pythonicway. After importing the module mymodule as in the example above, entitiesin the module could be accessed as:

${mymodule.SOME_CONSTANT}$$:mymodule.SOME_CONSTANT$:mymodule.some_function()@:mymodule.some_function()#:call mymodule.some_function#:endcall mymodule.some_function#:block mymodule.some_function#:endblock mymodule.some_function

Eval directive

A result of a Python expression can be inserted into the code by using evaldirectives $: (line form) or ${ and }$ (inline form). The expressionis evaluated using Python’s built-in function eval(). If it evaluates toNone, no output is produced. Otherwise the result is converted to a string andwritten to the output. The eval directive has both, a line and an inlinevariant:

$:somePythonFunction()print *, "DEBUG LEVEL: ${DEBUG}$"

Warning

Lines containing eval directive(s) will be folded usingFortran continuation lines when getting longer than a specified maximum. Theymust, therefore, not contain anything which could lead to invalid sourcecode, when being folded at an arbitrary position (e.g. Fortran comments).

set directive

The value of a variable can be set during the preprocessing via the setdirective. (Otherwise, variables can be also declared and defined via commandline options.) The first argument is the name of the variable (unquoted),followed by an optional Python expression. If the Python expression is present,it must be separated by an equal sign from the variable name. If the Pythonexpression and the equal sign are not present, the variable is set to None:

#:set DEBUG#:set LOG = 1#:set LOGLEVEL = LOGLEVEL + 1

Note, that in the last example the variable LOGLEVEL must have been alreadydefined in advance.

The set directive also accepts assignments to variable tuples, provided theright hand side of the assignment is compatible with the variable tuple:

#:set VAR1, VAR2 = 1, 2#:set (VAR1, VAR2) = 1, 2

The parantheses around the variable list (second example) are optional.

The set directive can be also used in the inline form:

#{set X = 2}#print *, ${X}$

Similar to the line form, the separating equal sign is optional here as well.

del directive

A variable (or macro) definition can be removed from the current scope by thedel directive:

#:set X = 12#! X available, with value 12:#:del X#! X not available any more

The variable name expression syntax is identical to the one used for the setdirective, so that also variable tuples can be deleted:

#! Removes the variables X and Y from local scope#:del X, Y

The variable passed to the del directive must exist and be erasable. So theexample above would trigger an error, if the variables X and Y were notdefined before.

The del directive can also be used to delete macro definitions:

#:def echo(TXT)${TXT}$#:enddef@:echo(HELLO)#:del echo#! Following line throws an error as macro echo is not available any more@:echo(HELLO)

The del directive can be also used in the inline form:

#{del X}#

if directive

Conditional output can be generated using the if directive. The condition mustbe a Python expression, which can be converted to a bool. If the conditionevaluates to True, the enclosed code is written to the output, otherwise it isignored.

print *, "Before"#:if DEBUG > 0print *, "Debug code"#:endifprint *, "After"

would result in

print *, "Before"print *, "Debug code"print *, "After"

if the Python expression DEBUG > 0 evaluates to True, otherwise in

print *, "Before"print *, "After"

For more complex scenarios elif and else branches can beused as well:

#:if DEBUG >= 2print *, "Very detailed debug info"#:elif DEBUG >= 1print *, "Less detailed debug info"#:elseprint *, "No debug info"#:endif

The if directive is also available as inline directive:

print *, "COMPILATION MODE: #{if DEBUG > 0}#DEBUG#{else}#PRODUCTION#{endif}#"

for directive

Fortran templates can be easily created by using the for directive. Thefollowing example creates a function for calculating the sine square for bothsingle and double precision reals:

#:set real_kinds = ['sp', 'dp']interface sin2#:for rkind in real_kinds module procedure sin2_${rkind}$#:endforend interface sin2#:for rkind in real_kindsfunction sin2_${rkind}$(xx) result(res) real(${rkind}$), intent(in) :: xx real(${rkind}$) :: res res = sin(xx) * sin(xx)end function sin2_${rkind}$#:endfor

The for directive expects a loop variable expression and an iterableseparated by the in keyword. The code within the for directive is outputedfor every iteration with the current value of the loop variable, which can beinserted using eval directives. The loop variable expression must be either aname or a list of names joined by comma (,). In the latter case, theiterable must consist of iterable items (e.g. tuples), which will be thenunpacked into the loop variables. (The number of the loop variables and thenumber of the components of each iterated item must be identical.):

#:set kinds = ['sp', 'dp']#:set names = ['real', 'dreal']#! create kinds_names as [('sp', 'real'), ('dp', 'dreal')]#:set kinds_names = list(zip(kinds, names))#! Access by indexinginterface sin2#:for kind_name in kinds_names module procedure sin2_${kind_name[1]}$#:endforend interface sin2#! Unpacking in the loop header#:for kind, name in kinds_namesfunction sin2_${name}$(xx) result(res) real(${kind}$), intent(in) :: xx real(${kind}$) :: res res = sin(xx) * sin(xx)end function sin2_${name}$#:endfor

The for directive can be used also in its inline form:

print *, "Numbers: #{for i in range(5)}#${i}$#{endfor}#"

def directive

Parametrized macros can be defined with the def directive. This defines aregular callable in Python, which returns the rendered content of the macro bodywhen called. The macro arguments are converted to local variables containing theactual arguments as values. The macro can be called from within aneval-directive, via the call and block control directives and via theirabreviated form, the direct call.

Given the macro definition

#:def ASSERT(cond)#:if DEBUG > 0if (.not. (${cond}$)) then print *, "Assert failed!" error stopend if#:endif#:enddef

the following three calls

#! call macro by evaluating a Python expression$:ASSERT('x > y')#! call macro by using the call directive (see below)#:call ASSERTx > y#:endcall ASSERT#! call macro by using the block directive (see below)#:block ASSERTx > y#:endblock ASSERT#! call macro by using the direct call directive (see below)@:ASSERT(x > y)

would all yield

if (.not. (x > y)) then print *, "Assert failed!" error stopend if

if the variable DEBUG had a value greater than zero or an empty stringotherwise.

It is possible to declare default values for the positional arguments of amacro. If for a given positional argument such a value is provided, then defaultvalues must be provided for all following arguments as well. When the macro iscalled, missing positional arguments will be replaced by their default value:

#:def macro(X, Y=2, Z=3)X=${X}$, Y=${Y}$, Z=${Z}$#:enddef macro$:macro(1) #! Returns "X=1, Y=2, Z=3"

Similar to Python, it is also possible to define macros with a variable numberof positional or keyword arguments (variadic macros) using the * and **argument prefixes. The corresponding arguments will contain the unprocessedpositional and keywords arguments as a list and a dictionary, respectively:

#:def macro(X, *VARPOS, **VARKW)pos: ${X}$varpos: #{for ARG in VARPOS}#${ARG}$, #{endfor}#varkw: #{for KEYWORD in VARKW}#${KEYWORD}$->${VARKW[KEYWORD]}$, #{endfor}##:enddef macro

Calling the example macro above with

$:macro(1, 2, 3, kw1=4, kw2=5)

yields:

pos: 1varpos: 2, 3,varkw: kw1->4, kw2->5,

Macros can be invoked recursively. Together with the variadic arguments, thisenables the realization of variadic templates (similar to C++) [2]:

#:def horner(x, a, b, *args)#:set res = "({} * {} + ({}))".format(a, x, b)#:if len(args) > 0 #:set res = horner(x, res, args[0], *args[1:])#:endif $:res#:enddef

Calling the horner macro with

poly = @{horner(x, 2, -3, 4, -5, 6)}@

would result in the Horner scheme with the specified coefficients:

poly = ((((2 * x + (-3)) * x + (4)) * x + (-5)) * x + (6))

Scopes

Scopes in general follow the Python convention: Within the macro, all variablesfrom the encompassing scope are available (as DEBUG in the example above), andadditionally those which were passed as arguments. If a variable is definedwithin the macro, it will be only accessible within the macro. If a variablewith the same name already exists in the encompassing scope, it will be shadowedby it for the time of the macro substitution. For example preprocessing the codesnippet

#:def macro(x)print *, "Local XY: ${x}$ ${y}$"#:set y = -2print *, "Local XY: ${x}$ ${y}$"#:enddef#:set x = 1#:set y = 2print *, "Global XY: ${x}$ ${y}$"$:macro(-1)print *, "Global XY: ${x}$ ${y}$"

would result in

print *, "Global XY: 1 2"print *, "Local XY: -1 2"print *, "Local XY: -1 -2"print *, "Global XY: 1 2"

For better readability, you can repeat the name of the macro (but not itsargument list) at the corresponding enddef directive:

#:def ASSERT(cond) #:if DEBUG > 0 if (.not. (${cond}$)) then print *, "Assert failed!" error stop end if #:endif#:enddef ASSERT

The def directive has no inline form.

Warning

The content of macros is usually inserted via an eval directive andis accordingly subject to eventual line folding. Macros should,therefore, not contain any inline Fortran comments. (Commentsstarting at the beginning of the line preceded by optionalwhitespaces only are OK, though). Use preprocessor comments(#!) instead.

block and call directives

When a Python callable (regular Python function, macro etc.) needs a stringargument of larger size (e.g. source code), it can be called using the call orthe block directives to avoid extra quoting of the text argument and to enablepassing of multiline arguments in a comfortable way:

#:def DEBUG_CODE(code) #:if DEBUG > 0 $:code #:endif#:enddef DEBUG_CODE#:block DEBUG_CODE if (a < b) then print *, "DEBUG: a is less than b" end if#:endblock DEBUG_CODE#:call DEBUG_CODE if (a < b) then print *, "DEBUG: a is less than b" end if#:endcall DEBUG_CODE

The block and the call directives are equivalent. The two alternative formsexists in order to allow for more readable meta-code depending on the context.

The block and call directives take the name of the callable as argument. Thelines between the opening and closing directives will be rendered and thenpassed as positional string arguments to the callable. The name of thecallable can be repeated in the endblock and endcall directives for enhancedreadability:

#! This form is probably somewhat more natural to read#:block DEBUG_CODE if (a < b) then print *, "DEBUG: a (${a}$) is less than b (${b}$)" end if#:endblock DEBUG_CODE#:call DEBUG_CODE if (a < b) then print *, "DEBUG: a (${a}$) is less than b (${b}$)" end if#:endcall DEBUG_CODE

If the callable needs more than one string arguments, the contains directive(for block) or the nextarg directive (for call) can be used to separatethe arguments from each other:

#:def CHOOSE_CODE(debug_code, nondebug_code) #:if DEBUG > 0 $:debug_code #:else $:nondebug_code #:endif#:enddef CHOOSE_CODE#:block CHOOSE_CODE if (a < b) then print *, "DEBUG: a is less than b" end if#:contains print *, "No debugging"#:endcall CHOOSE_CODE#! This form is probably somewhat more natural to read#:call CHOOSE_CODE if (a < b) then print *, "DEBUG: a is less than b" end if#:nextarg print *, "No debugging"#:endcall CHOOSE_CODE

The lines in the body of the block and call directives may containdirectives themselves. However, any variable defined within the body of theblock and call directives will be a local variable existing only during theevaluation of that branch of the directive (and not being available when thecallable is called with the evaluated string as argument).

The contains and nextarg directives may be followed by an optional argumentname. In that case the text following will be passed as keyword argument to thecallable. If the first argument should be also passed as keyword argument, itshould be also preceded by a named contains or nextarg directive declaredin the line immediately following the block or call directive. If anargument is passed as a keyword argument, all following arguments must be passedas keyword arguments as well:

#:block CHOOSE_CODE#:contains nondebug_code print *, "No debugging"#:contains debug_code if (a < b) then print *, "DEBUG: a is less than b" end if#:endblock CHOOSE_CODE#:call CHOOSE_CODE#:nextarg nondebug_code print *, "No debugging"#:nextarg debug_code if (a < b) then print *, "DEBUG: a is less than b" end if#:endcall CHOOSE_CODE

Additional to passing the content of the block or call directives body asstring argument, further arguments of arbitrary type can be passed by specifyingthem directly in the header of the directive. Among others, this can be verycomfortable when the callable needs also non-string type of arguments:

#! Argument 'repeat' should be an integer, not string#:def REPEAT_CODE(code, repeat) #:for ind in range(repeat) $:code #:endfor#:enddef REPEAT_CODE#! Code block as positional argument and 3 as keyword argument "repeat"#:block REPEAT_CODE(repeat=3)this will be repeated 3 times#:block REPEAT_CODE#! Code block as positional argument and 3 as keyword argument "repeat"#:call REPEAT_CODE(repeat=3)this will be repeated 3 times#:endcall REPEAT_CODE

The arguments must be specified between parantheses and are evaluated as Pythonexpressions. The arguments specified in the directive (both, in the header andin the body) are passed to the callable in the following order:

  1. positional arguments in the header

  2. positional arguments in the body

  3. keyword arguments in the header

  4. keyword arguments in the body

Callables without arguments can also be called with the block and calldirectives, provided the endblock and endcall directives immediately followsthe opening directive. If there are empty lines between the opening and theclosing directives, they will be interpreted as a positional argument:

#:def macro_noarg()NOARGS#:enddef macro_noarg#:def macro_arg1(arg1)ARG1:${arg1}$#:enddef macro_arg1#! Calling macro without arguments#:block macro_noarg#:endblock macro_noarg#! Calling macro without arguments#:call macro_noarg#:endcall macro_noarg#! Calling macro with one positional (empty) argument#! Note the empty line between block and endblock#:block macro_arg1#:endblock macro_arg1#! Calling macro with one positional (empty) argument#! Note the empty line between call and endcall#:call macro_arg1#:endcall macro_arg1

The block and call directives can also be used in their inline form. As thiseasily leads to code being hard to read, it should be usually avoided:

! Rather uglyprint *, #{block CHOOSE_CODE}# a(:) #{contains}# size(a) #{endblock}#! Rather ugly as wellprint *, #{call CHOOSE_CODE}# a(:) #{nextarg}# size(a) #{endcall}#! This form is more readableprint *, ${CHOOSE_CODE('a(:)', 'size(a)')}$! Alternatively, you may use a direct call (see next section)print *, @{CHOOSE_CODE(a(:), size(a))}@

If the callable only requires short text arguments, the more compact direct calldirective should be used as an alternative (see next section).

Direct call directive

In order to enable compact (single line) calls while still maintaining codereadability, the block and call directives have an alternative form, thedirect call directive:

#:def ASSERT(cond) #:if DEBUG > 0 if (.not. (${cond}$)) then print *, "Assert failed!" error stop end if #:endif#:enddef ASSERT@:ASSERT(size(aa) >= size(bb))

The direct call directive starts with @: followed by the name of a Pythoncallable and an opening parenthesis ((). Everything after that up to theclosing parenthesis (``)``) is passed as string argument to the callable. Theclosing parenthesis may only be followed by whitespace characters.

When the callable needs more than one argument, the arguments must be separatedby a comma (,):

#:def ASSERT_EQUAL(received, expected) if (${received}$ /= ${expected}$) then print *, "ASSERT_EQUAL failed (${received}$ /= ${expected}$)!" error stop end if#:enddef ASSERT_EQUAL@:ASSERT_EQUAL(size(coords, dim=2), size(atomtypes))

Note

In order to be able to split the argument string of a direct callcorrectly, Fypp assumes that all provided arguments represent validFortran expressions with balanced quotes (' or ") and balancedbrackets ((), [] and {}) outside of quoted regions. Theargument string is only split around commas which are outside of anyquoted or bracketed regions.

Arguments can be optionally enclosed within curly braces in order to avoidargument splitting at unwanted places or to improve readability. The outermostcurly braces will be removed from the arguments before they are passed to thecallable:

#! Passes "a**2 + b**2" and "c**2" as string arguments to ASSERT_EQUAL@:ASSERT_EQUAL({a**2 + b**2}, c**2)

Keywords arguments can be passed by prefixing them with the keyword nameand an equal sign:

@:ASSERT_EQUAL(expected=size(atomtypes), received=size(coords, dim=2))@:ASSERT_EQUAL(expected=c**2, received={a**2 + b**2})

If the equal sign is followed immediately by an other equal sign, the argumentwill be recognized as positional and not as keyword argument. This exceptionallows for passing valid Fortran code containing the comparison operator(==) without the need for special bracketing. In other cases, however,bracketing may be needed to avoid recognition as keyword argument:

#! Passes string "a == b" as first positional argument@:ASSERT(a == b)#! Passes string "=b" as keyword argument "a"@:ASSERT(a={=b})#! Passes string "b" as keyword argument "a"@:someMacro(a = b)#! Passes "a = b" as positional argument@:someMacro({a = b})

The direct call directive may contain continuation lines:

@:ASSERT_EQUAL(size(coords, dim=2), & & size(atomtypes))

The arguments are parsed for further inline eval directives (but not for anyinline control or direct call directives), making variable substitutions in thearguments possible:

#:set MYSIZE = 2@:ASSERT_EQUAL(size(coords, dim=2), ${MYSIZE}$)

Whitespaces around the arguments of the direct call are stripped, but not thewhitespaces within the optional curly braces around the argument:

#! Calls a macro without arguments@:macro_without_args()#! Calls a macro with no arguments (whitespace between () is stripped):@:macro_without_args( )#! Calls a macro with empty string as argument@:macro_with_one_arg({})#! Calls a macro with one space as argument@:macro_with_one_arg({ })

The direct call directive can also be used in its inline form:

#! Using CHOOSE_CODE() macro defined in previous sectionprint *, @{CHOOSE_CODE(a(:), size(a))}@

global directive

Global variables are by default read-only in local scopes (e.g. withinmacros). This can be changed for selected variables by using the globaldirective:

#:def set_debug(value) #:global DEBUG #:set DEBUG = value#:enddef set_debug#:set DEBUG = 1$:DEBUG$:set_debug(2)$:DEBUG

In the example above, without the global directive, the set directive wouldhave created a local variable within the macro, which had shadowed the globalvariable and was destroyed at the end of the macro execution. With the globaldirective the set refers to the variable in the global scope. Thevariable in the global scope does not need to exist yet, when the globaldirective is executed. It will be then created at the first set directive, orremain non-existing if no assignment is made in the current scope.

A variable can only made global, if it was not created in the local scopeyet. Therefore, the following code would throw an exception:

#:def set_debug(value) #! DEBUG variable created in local scope #:set DEBUG = value #! Invalid: variable DEBUG already exists in local scope #:global DEBUG#:enddef set_debug# Throws exception$:set_debug(2)

include directive

The include directive allows you to collect your preprocessor macros andvariable definitions in separate files and include them whenever needed. Theinclude directive expects a quoted string with a file name:

#:include 'mydefs.fypp'

If the file name is relative, it is interpreted relative to the folder where theprocessed file is located (or to the current folder, if Fypp reads fromstdin). Further lookup paths can be added with the -I command line option.

The include directive does not have an inline form.

mute directive

Empty lines between Fypp definitions makes the code easier to read. However,being outside of Fypp-directives, those empty lines will be written unaltered tothe output. This can be especially disturbing if various macro definitionfiles are included, as the resulting output would eventually contain a lot ofempty lines. With the mute directive, the output can be suspended. Whileeverything is still processed as normal, no output is written for the codewithin the mute directive:

#:mute#:include "mydefs1.fypp"#:include "mydefs2.fypp"#:def test(x)print *, "TEST: ${x}$"#:enddef test#:endmute$:test('me')

The example above would only produce

print *, "TEST: me"

as output without any newlines.

The mute directive does not have an inline form.

stop directive

The stop directive can be used to report an error and stop the preprocessorbefore all input has been consumed. This can be useful in cases, where someexternal conditions (e.g. user defined variables) do not meet certaincriteria. The directive expects a Python expression, which will be converted tostring and written to standard error. After writing the error message Fypp exitsimmediately with a non-zero exit code (see Exit Codes):

#! Stop the code if DEBUGLEVEL is not positive#:if DEBUGLEVEL < 0 #:stop 'Wrong debug level {}!'.format(DEBUGLEVEL)#:endif

There is no inline form of the stop directive.

assert directive

The assert directive is a short form for the combination of an if and astop directive. It evaluates a given expression and stops the code if theboolean value of the result is False. This can be very convenient, if you wantto write robust macros containing sanity checks for their arguments:

#:def mymacro(RANK) #! Macro only works for RANK 1 and above #:assert RANK > 0 :#:enddef mymacro

Given the macro definition above, the macro call

$:mymacro(1)

would pass the assert directive in the third line, while the call

$:mymacro(0)

would cause Fypp to stop at it.

When the expression in an assert directive evaluates to False, Fypp reportsthe failed assertion (the condition, the file name and the line number) onstandard error and terminates immediately with a non-zero exit code (see ExitCodes).

There is no inline form of the assert directive.

Comment directive

Comment lines can be added by using the #! preprocessor directive. Thecomment line (including the newlines at their end) will be ignored by theprepropessor and will not appear in the output:

#! This will not show up in the output

There is no inline form of the comment directive.

Multiline directives

The line form of the control and eval directives can span arbitrary number oflines, if Fortran-style continuation characters are used:

#:if a > b & & or b > c & & or c > d$:somePythonFunction(param1, & &param2)

The line break at the first line must be in the expression, not in the openingdelimiter characters or in the directive name. Similar to Fortran, thecontinuation character at the beginning of each continuation line may be leftaway, but then all whitespaces at the beginning of the respective continuationline will be part of the expression.

Inline directives must not contain any continuation lines.

Line folding

The Fortran standard only allows source lines up to 132 characters. In order toemit standard conforming code, Fypp folds all lines in the output which it hadmanipulated before (all lines containing eval directives). Lines which werejust copied to the output are left unaltered. The maximal line length can bechosen by the -l command line option. The indentation of the continuationlines can be tuned with the --indentation option, and the folding strategycan be selected by the -f option with following possibilities:

  • brute: Continuation lines are indented relative to the beginning ofthe line, and each line is folded at the maximal line position.

  • simple: Like brute, but continuation lines are indented with respectof the indentation of the original line.

  • smart: Like simple, but Fypp tries to fold the line at a whitespacecharacter in order to prevent split tokens. To prevent continuation linesbecoming too short, it defaults to simple if no whitespace occurs in thelast third of the line.

The -F option can be used to turn off line folding.

Warning

Fypp is not aware of the Fortran semantics of the lines it folds.

Fypp applies the line folding mechanically (only considering the position of thewhitespace characters). Lines containing eval directives and lines within macrodefinitions should, therefore, not contain any Fortran style comments (startedby !) within the line, as folding within the comment would result ininvalid Fortran code. For comments within such lines, Fypps comment directive(#!) can be used instead:

#:def macro()print *, "DO NOT DO THIS!" ! Warning: Line may be folded within the commentprint *, "This is OK." #! Preprocessor comment is safe as it will be stripped

For comments starting at the beginning of the line (preceded by optionalwhitespace characters only) the folding is suppressed, though. This enables youto define macros with non-negligible comment lines (e.g. with source codedocumentation or OpenMP directives):

#:def macro(DTYPE)!> This functions calculates something (version ${DTYPE}$)!! \param xx Ingoing value!! \return Some calculated value.${DTYPE}$ function calcSomething(xx):end function calcSomething#:enddef macro

Escaping

If you want to prevent Fypp to interpret something as a directive, put abackslash (\) between the first and second delimiter character. In case ofinline directives, do it for both, the opening and the closing delimiter:

$\: 1 + 2#\{if 1 > 2}\#@\:myMacro arg1

Fypp will not recognize the escaped strings as directives, but will remove thebackslash between the delimiter characters in the output. If you put more thanone backslash between the delimiters, only one will be removed.

Line numbering markers

In order to support compilers in emitting messages with correct line numberswith respect to the original source file, Fypp can put line number directives(a.k.a. line markers) in its output. This can be enabled by using the commandline option -n. Given a file test.fpp with the content

program test#:if defined('MPI')use mpi#:elseuse openmpi#:endif:end program test

the command

fypp -n -DMPI test.fpp

produces the output

# 1 "test.fpp" 1program test# 3 "test.fpp" use mpi# 7 "test.fpp":end program test

If during compilation of this output an error occurred in the line use mpi(e.g. the mpi module can not be found), the compiler would know that this linecorresponds to line number 3 in the original file test.fpp and could emit anaccording error message.

The line numbering directives can be fine tuned with the -N option, whichaccepts following mode arguments:

  • full: Line numbering directives are emitted whenever lines areremoved from the original source file or extra lines are added to it.

  • nocontlines: Same as full, but line numbering directives are omittedbefore continuation lines. (Some compilers, like the NAG Fortran compiler,have difficulties with line numbering directives before continuation lines).

Note: Due to a bug introduced in GFortran 5 (being also present in majorversions 6), a workaround is needed for obtaining correct error messages whencompiling preprocessed files with those compilers. Please use the command lineoption --line-marker-format 'gfortran5' in those cases.

Scopes

Fypp uses a scope concept very similar to Pythons one. There is one global scope(like in Python modules), and temporary local scopes may be created in specialcases (e.g. during macro calls).

The global scope is the one, which Fypp normaly uses for defining objects. Allimports specified on the command line are carried out in this scope And alldefinitions made by the set and def directives in the processed source filedefines entities in that scope, unless they appear within a block, a call ora def directive.

Addtional temporary local scopes are opened, whenever

  • a macro defined by the def directive is called, or

  • the body of the block or call directive is evaluated in order to renderthe text, which will be passed to the callable as argument.

Any entity defined in a local scope is only visible within that scope and isunaccessible once the scope has been closed. For example the code snippet:

#:set toupper = lambda s: s.upper()#:call toupper#:set NUMBER = 9here is the number ${NUMBER}$#:endcall toupper$:defined('NUMBER')

results after preprocessing in

HERE IS THE NUMBER 9False

as the variable NUMBER defined in the local scope is destroyed, when thescope is closed (the endcall directive has been reached).

Lookup rules

When Fypp tries to resolve a name, the lookup rules depend on the scope, inwhich the query appears:

  • global scope (outside of any def or call directives): only the globalscope is searched.

  • local scope (within the body of a call or def directive): first, theactive local scope is searched. Then the scope embedding it (the scope whichcontains the directive) is searched. Then further embedding scopes aresearched until finally also the global scope has been checked. The search isimmediately terminated, if the name has been found in a scope.

Note, that all variables outside of the active scope are read-only. If avariable with the same name is created in the active scope, it will shadow theoriginal definition. Once the scope is closed, the variable regains it originalvalue. For example:

#:set toupper = lambda s: s.upper()#:set X = 1#:call toupper#:set X = 2value ${X}$#:endcall touppervalue ${X}$

results in

VALUE 2value 1

Also note, that if a name can not be resolved in the active scope during a macroevaluation, the relevant embedding scope for the next lookup is the scope, wherethe macro has been defined (where the def directive occurs), and not thescope, from which the macro is being called. The following snippet demonstratesthis:

#! GLOBAL SCOPE#:set toupper = lambda s: s.upper()#:call toupper#! LOCAL SCOPE 1#:def macro1()#! LOCAL SCOPE 2Avalue of x: ${X}$#:enddef macro1#! LOCAL SCOPE 1#:def macro2()#! LOCAL SCOPE 2B#:set X = 2$:macro1()#:enddef macro2#! LOCAL SCOPE 1#:set X = 1$:macro2()#:endcall#! GLOBAL SCOPE

After processing the code above one obtains VALUE OF X: 1. Although in thelocal scope 2B, from where the macro macro1() is called, the value of X isdefined to be 2, the relevant scopes for the lookup of X during the macroevaluation are the local scope 2A of macro1() (where the eval-directive forX is located), the local scope 1 (where the def directive for macro1()occurs) and the global scope (which embeds local scope 1). Therefore, at themacro evaluation the value 1 will be substituted as this is the value of Xin scope 1, and scope 1 is the first scope in the lookup order, which provides avalue for X.

Rendering file names as relative paths

When the input file is specified as an absolute path (e.g. during anout-of-source build), the variables _FILE_ and _THIS_FILE_ will alsocontain absolute paths. This might result in file names, which are unnecessarylong and might reveal unwanted information about the directory structure on thebuilding host.

The --file-var-root option converts the paths in _FILE_ and_THIS_FILE_ to relative paths with respect to a specified root folder.Given the file source.fpp:

[...]call fatal_error("Error in ${_FILE_}$:${_LINE_}$")

invoking with Fypp with

fypp /home/user/projectdir/src/source.fpp

results in

[...]call fatal_error("Error in /home/user/projectdir/src/source.fpp:2")

while using the --file-var-root option

fypp --file-var-root=/home/user/projectdir /home/user/projectdir/src/source.fpp

yields

[...]call fatal_error("Error in src/source.fpp:2")

Exit codes

When run as a standalone application, Fypp returns one of the following exitcodes to the calling environment:

  • 0: Preprocessing finished successfully.

  • 1: Stopped due to an unexpected error.

  • 2: Explicitely requested stop encountered (stop directive or assertdirective).

Asserts and debug code

In this example a simple “assert”-mechanism (as can be found in many programminglanguages) should be implemented, where run-time checks can be included orexcluded depending on preprocessor variable definitions. Apart of singleassert-like queries, we also want to include larger debug code pieces, which canbe removed in the production code.

First, we create an include file (checks.fypp) with the appropriate macros:

#:mute#! Enable debug feature if the preprocessor variable DEBUG has been defined#:set DEBUG = defined('DEBUG')#! Stops the code, if the condition passed to it is not fulfilled#! Only included in debug mode.#:def ASSERT(cond, msg=None) #:if DEBUG if (.not. (${cond}$)) then write(*,*) 'Run-time check failed' write(*,*) 'Condition: ${cond.replace("'", "''")}$' #:if msg is not None write(*,*) 'Message: ', ${msg}$ #:endif write(*,*) 'File: ${_FILE_}$' write(*,*) 'Line: ', ${_LINE_}$ stop end if #:endif#:enddef ASSERT#! Includes code if in debug mode.#:def DEBUG_CODE(code) #:if DEBUG$:code #:endif#:enddef DEBUG_CODE#:endmute

Remarks:

  • All macro definitions are within a #:mute#:endmute pair in order toprevent the appearance of disturbing empty lines (the lines between the macrodefinitions) in the file which includes checks.fypp.

  • The preprocessor variable DEBUG will determine, whether the checksand the debug code is left in the preprocessed code or not.

  • The content of both macros, ASSERT and DEBUG_CODE, are only includedif the variable DEBUG has been defined.

  • We also want to print out the failed condition for more verbose output. As thecondition may contains apostrophes, we use Python’s string replacement methodto escape them.

With the definitions above, we can use the functionality in any Fortran sourceafter including checks.fypp:

#:include 'checks.fypp'module testmod implicit nonecontains subroutine someFunction(ind, uplo) integer, intent(in) :: ind character, intent(in) :: uplo @:ASSERT(ind > 0, msg="Index must be positive") @:ASSERT(uplo == 'U' .or. uplo == 'L') ! Do something useful here ! : #:block DEBUG_CODE print *, 'We are in debug mode' print *, 'The value of ind is', ind #:endblock DEBUG_CODE end subroutine someFunctionend module testmod

Now, the file testmod.fpp can be preprocessed with Fypp. When the variableDEBUG is not set:

fypp testmod.fpp testmod.f90

the resulting routine will not contain the conditional code:

subroutine someFunction(ind, uplo) integer, intent(in) :: ind character, intent(in) :: uplo ! Do something useful here ! :end subroutine someFunction

On the other hand, if the DEBUG variable is set:

fypp -DDEBUG testmod.fpp testmod.f90

the run-time checks and the debug code will be there:

 subroutine someFunction(ind, uplo) integer, intent(in) :: ind character, intent(in) :: uploif (.not. (ind > 0)) then write(*,*) 'Run-time check failed' write(*,*) 'Condition: ind > 0' write(*,*) 'Message: ', "Index must be positive" write(*,*) 'File: testmod.fpp' write(*,*) 'Line: ', 12 stopend ifif (.not. (uplo == 'U' .or. uplo == 'L')) then write(*,*) 'Run-time check failed' write(*,*) 'Condition: uplo == ''U'' .or. uplo == ''L''' write(*,*) 'File: testmod.fpp' write(*,*) 'Line: ', 13 stopend if ! Do something useful here ! : print *, 'We are in debug mode' print *, 'The value of ind is', ind end subroutine someFunction

Generic programming

The example below shows how to create a generic function maxRelError(),which gives the maximal elementwise relative error for any pair of arrays withranks from 0 (scalar) to 7 in single or double precision. The Fortran module(file errorcalc.fpp) contains the interface maxRelError which maps toall the realizations with the different array ranks and precisions:

#:def ranksuffix(RANK)$:'' if RANK == 0 else '(' + ':' + ',:' * (RANK - 1) + ')'#:enddef ranksuffix#:set PRECISIONS = ['sp', 'dp']#:set RANKS = range(0, 8)module errorcalc implicit none integer, parameter :: sp = kind(1.0) integer, parameter :: dp = kind(1.0d0) interface maxRelError #:for PREC in PRECISIONS #:for RANK in RANKS module procedure maxRelError_${RANK}$_${PREC}$ #:endfor #:endfor end interface maxRelErrorcontains#:for PREC in PRECISIONS #:for RANK in RANKS function maxRelError_${RANK}$_${PREC}$(obtained, reference) result(res) real(${PREC}$), intent(in) :: obtained${ranksuffix(RANK)}$ real(${PREC}$), intent(in) :: reference${ranksuffix(RANK)}$ real(${PREC}$) :: res #:if RANK == 0 res = abs((obtained - reference) / reference) #:else res = maxval(abs((obtained - reference) / reference)) #:endif end function maxRelError_${RANK}$_${PREC}$ #:endfor#:endforend module errorcalc

The macro ranksuffix() defined at the beginning receives a rank as argumentand returns a string, which is either the empty string (rank 0) or theappropriate number of dimension placeholder separated by commas and withinparantheses (e.g. (:,:) for rank 2). The string expression is calculated asa Python expression, so that we can make use of the powerful string manipulationroutines in Python and write it as a one-line routine.

If we preprocess the Fortran source file errorcalc.fpp with Fypp:

fypp errorcalc.fpp errorcalc.f90

the resulting file errorcalc.f90 will contain a module with the genericinterface maxRelError():

interface maxRelError module procedure maxRelError_0_sp module procedure maxRelError_1_sp module procedure maxRelError_2_sp module procedure maxRelError_3_sp module procedure maxRelError_4_sp module procedure maxRelError_5_sp module procedure maxRelError_6_sp module procedure maxRelError_7_sp module procedure maxRelError_0_dp module procedure maxRelError_1_dp module procedure maxRelError_2_dp module procedure maxRelError_3_dp module procedure maxRelError_4_dp module procedure maxRelError_5_dp module procedure maxRelError_6_dp module procedure maxRelError_7_dpend interface maxRelError

The interface maps to the appropriate functions:

function maxRelError_0_sp(obtained, reference) result(res) real(sp), intent(in) :: obtained real(sp), intent(in) :: reference real(sp) :: res res = abs((obtained - reference) / reference)end function maxRelError_0_spfunction maxRelError_1_sp(obtained, reference) result(res) real(sp), intent(in) :: obtained(:) real(sp), intent(in) :: reference(:) real(sp) :: res res = maxval(abs((obtained - reference) / reference))end function maxRelError_1_spfunction maxRelError_2_sp(obtained, reference) result(res) real(sp), intent(in) :: obtained(:,:) real(sp), intent(in) :: reference(:,:) real(sp) :: res res = maxval(abs((obtained - reference) / reference))end function maxRelError_2_sp:

The function maxRelError() can be, therefore, invoked with a pair of arrayswith various ranks or with a pair of scalars, both in single and in doubleprecision, as required.

If you prefer not to have preprocessor loops around long code blocks, theexample above can be also written by defining a macro first and then callingthe macro within the loop. The function definition would then look as follows:

contains#:def maxRelError_template(RANK, PREC) function maxRelError_${RANK}$_${PREC}$(obtained, reference) result(res) real(${PREC}$), intent(in) :: obtained${ranksuffix(RANK)}$ real(${PREC}$), intent(in) :: reference${ranksuffix(RANK)}$ real(${PREC}$) :: res #:if RANK == 0 res = abs((obtained - reference) / reference) #:else res = maxval(abs((obtained - reference) / reference)) #:endif end function maxRelError_${RANK}$_${PREC}$#:enddef maxRelError_template#:for PREC in PRECISIONS #:for RANK in RANKS $:maxRelError_template(RANK, PREC) #:endfor#:endforend module errorcalc

Fypp can be integrated into build environments like any other preprocessor. Ifyour build environment is Python-based, you may consider to access itsfunctionality directly via its API instead of calling it as an external script(see the API documentation).

Make

In traditional make based system you can define an appropriate preprocessorrule in your Makefile:

.fpp.f90: fypp $(FYPPFLAGS) $< $@

or for GNU make:

%.f90: %.fpp fypp $(FYPPFLAGS) $< $@

Waf

For the waf build system the Fypp source tree contains extension modules inthe folder tools/waf. They use Fypps Python API, therefore, the fyppmodule must be accessible from Python. Using those waf modules, you canformulate a Fypp preprocessed Fortran build like the example below:

def options(opt): opt.load('compiler_fc') opt.load('fortran_fypp')def configure(conf): conf.load('compiler_fc') conf.load('fortran_fypp')def build(bld): sources = bld.path.ant_glob('*.fpp') bld( features='fypp fc fcprogram', source=sources, target='myprog' )

Check the documentation in the corresponding waf modules for further details.

CMake

One possible way of invoking the Fypp preprocessor within the CMake buildframework is demonstrated below (thanks to Jacopo Chevallard for providing thevery first version of this example):

### Pre-process: .fpp -> .f90 via Fypp# Create a list of the files to be preprocessedset(fppFiles file1.fpp file2.fpp file3.fpp)# Pre-processforeach(infileName IN LISTS fppFiles) # Generate output file name string(REGEX REPLACE ".fpp\$" ".f90" outfileName "${infileName}") # Create the full path for the new file set(outfile "${CMAKE_CURRENT_BINARY_DIR}/${outfileName}") # Generate input file name set(infile "${CMAKE_CURRENT_SOURCE_DIR}/${infileName}") # Custom command to do the processing add_custom-command( OUTPUT "${outfile}" COMMAND fypp "${infile}" "${outfile}" MAIN_DEPENDENCY "${infile}" VERBATIM) # Finally add output file to a list set(outFiles ${outFiles} "${outfile}")endforeach(infileName)

Additional to its usage as a command line tool, Fypp can also be operateddirectly from Python. This can be especially practical, when Fypp is used in aPython driven build environment (e.g. waf, Scons). Below you find the detaileddocumentation of the API Fypp offers.

fypp module

For using the functionality of the Fypp preprocessor from withinPython, one usually interacts with the following two classes:

  • Fypp: The actual Fypp preprocessor. It returns for a given inputthe preprocessed output.

  • FyppOptions: Contains customizable settings controlling the behaviour ofFypp. Alternatively, the function get_option_parser() can be used toobtain an option parser, which can create settings based on command linearguments.

If processing stops prematurely, an instance of one of the followingsubclasses of FyppError is raised:

  • FyppFatalError: Unexpected error (e.g. bad input, missing files, etc.)

  • FyppStopRequest: Stop was triggered by an explicit request in the input(by a stop- or an assert-directive).

Fypp

class fypp.Fypp(options=None, evaluator_factory=<class 'fypp.Evaluator'>, parser_factory=<class 'fypp.Parser'>, builder_factory=<class 'fypp.Builder'>, renderer_factory=<class 'fypp.Renderer'>)

Fypp preprocessor.

You can invoke it like

tool = fypp.Fypp()tool.process_file('file.in', 'file.out')

to initialize Fypp with default options, process file.in and write theresult to file.out. If the input should be read from a string, theprocess_text() method can be used:

tool = fypp.Fypp()output = tool.process_text('#:if DEBUG > 0\nprint *, "DEBUG"\n#:endif\n')

If you want to fine tune Fypps behaviour, pass a customized FyppOptionsinstance at initialization:

options = fypp.FyppOptions()options.fixed_format = Truetool = fypp.Fypp(options)

Alternatively, you can use the command line parser optparse.OptionParserto set options for Fypp. The function get_option_parser() returns you adefault option parser. You can then use its parse_args() method toobtain settings by reading the command line arguments:

optparser = fypp.get_option_parser()options, leftover = optparser.parse_args()tool = fypp.Fypp(options)

The command line options can also be passed directly as a list whencalling parse_args():

args = ['-DDEBUG=0', 'input.fpp', 'output.f90']optparser = fypp.get_option_parser()options, leftover = optparser.parse_args(args=args)tool = fypp.Fypp(options)

For even more fine-grained control over how Fypp works, you can pass incustom factory methods that handle construction of the evaluator, parser,builder and renderer components. These factory methods must have the samesignature as the corresponding component’s constructor. As an example ofusing a builder that’s customized by subclassing:

class MyBuilder(fypp.Builder): def __init__(self): super().__init__() ...additional initialization...tool = fypp.Fypp(options, builder_factory=MyBuilder)
Parameters:
  • options (object) – Object containing the settings for Fypp. You typicallywould pass a customized FyppOptions instance or anoptparse.Values object as returned by the option parser. If notpresent, the default settings in FyppOptions are used.

  • evaluator_factory (function) – Factory function that returns an Evaluatorobject. Its call signature must match that of the Evaluatorconstructor. If not present, Evaluator is used.

  • parser_factory (function) – Factory function that returns a Parserobject. Its call signature must match that of the Parserconstructor. If not present, Parser is used.

  • builder_factory (function) – Factory function that returns a Builderobject. Its call signature must match that of the Builderconstructor. If not present, Builder is used.

  • renderer_factory (function) – Factory function that returns a Rendererobject. Its call signature must match that of the Rendererconstructor. If not present, Renderer is used.

process_file(infile, outfile=None)

Processes input file and writes result to output file.

Parameters:
  • infile (str) – Name of the file to read and process. If its value is‘-’, input is read from stdin.

  • outfile (str, optional) – Name of the file to write the result to.If its value is ‘-’, result is written to stdout. If notpresent, result will be returned as string.

  • env (dict, optional) – Additional definitions for the evaluator.

Returns:

Result of processed input, if no outfile was specified.

Return type:

str

process_text(txt)

Processes a string.

Parameters:
  • txt (str) – String to process.

  • env (dict, optional) – Additional definitions for the evaluator.

Returns:

Processed content.

Return type:

str

FyppOptions

class fypp.FyppOptions

Container for Fypp options with default values.

defines

List of variable definitions in the form of‘VARNAME=VALUE’. Default: []

Type:

list of str

includes

List of paths to search when looking for includefiles. Default: []

Type:

list of str

line_numbering

Whether line numbering directives should appearin the output. Default: False

Type:

bool

line_numbering_mode

Line numbering mode ‘full’ or ‘nocontlines’.Default: ‘full’.

Type:

str

line_marker_format

Line marker format. Currently ‘std’,‘cpp’ and ‘gfortran5’ are supported, where ‘std’ emits #linepragmas similar to standard tools, ‘cpp’ produces line directives asemitted by GNU cpp, and ‘gfortran5’ cpp line directives with aworkaround for a bug introduced in GFortran 5. Default: ‘cpp’.

Type:

str

line_length

Length of output lines. Default: 132.

Type:

int

folding_mode

Folding mode ‘smart’, ‘simple’ or ‘brute’. Default:‘smart’.

Type:

str

no_folding

Whether folding should be suppressed. Default: False.

Type:

bool

indentation

Indentation in continuation lines. Default: 4.

Type:

int

modules

Modules to import at initialization. Default: [].

Type:

list of str

moduledirs

Module lookup directories for importing userspecified modules. The specified paths are looked up before thestandard module locations in sys.path.

Type:

list of str

fixed_format

Whether input file is in fixed format.Default: False.

Type:

bool

encoding

Character encoding for reading/writing files. Allowedvalues are Pythons codec identifiers, e.g. ‘ascii’, ‘utf-8’, etc.Default: ‘utf-8’. Reading from stdin and writing to stdout is alwaysencoded according to the current locale and is not affected by thissetting.

Type:

str

create_parent_folder

Whether the parent folder for the outputfile should be created if it does not exist. Default: False.

Type:

bool

get_option_parser()

fypp.get_option_parser()

Returns an option parser for the Fypp command line tool.

Returns:
Parser which can create an optparse.Values object with

Fypp settings based on command line arguments.

Return type:

OptionParser

FyppError

class fypp.FyppError(msg, fname=None, span=None)

Signalizes error occurring during preprocessing.

Parameters:
  • msg (str) – Error message.

  • fname (str) – File name. None (default) if file name is not available.

  • span (tuple of int) – Beginning and end line of the region where erroroccurred or None if not available. If fname was not None, span mustnot be None.

msg

Error message.

Type:

str

fname

File name or None if not available.

Type:

str or None

span

Beginning and end line of the regionwhere error occurred or None if not available. Line numbers startfrom zero. For directives, which do not consume end of the line,start and end lines are identical.

Type:

tuple of int or None

Introduction — Fypp 3.2 documentation (2024)
Top Articles
Latest Posts
Article information

Author: Jeremiah Abshire

Last Updated:

Views: 6373

Rating: 4.3 / 5 (54 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Jeremiah Abshire

Birthday: 1993-09-14

Address: Apt. 425 92748 Jannie Centers, Port Nikitaville, VT 82110

Phone: +8096210939894

Job: Lead Healthcare Manager

Hobby: Watching movies, Watching movies, Knapping, LARPing, Coffee roasting, Lacemaking, Gaming

Introduction: My name is Jeremiah Abshire, I am a outstanding, kind, clever, hilarious, curious, hilarious, outstanding person who loves writing and wants to share my knowledge and understanding with you.