Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c39ec0
Added Directive node separated from comments
LonelyCat124 Jun 26, 2025
80a4d18
Merge branch 'master' into 468_directives
LonelyCat124 Jun 26, 2025
6fb2a20
Formatting
LonelyCat124 Jun 26, 2025
4b6bf3d
Merge branch '468_directives' of github.com:stfc/fparser into 468_dir…
LonelyCat124 Jun 26, 2025
a953ed3
Fix coverage and update docs
LonelyCat124 Jun 26, 2025
22d27a2
Changes for review
LonelyCat124 Jun 30, 2025
1e8c84b
changes for review
LonelyCat124 Jun 30, 2025
826f493
Formatting
LonelyCat124 Jun 30, 2025
af58dde
Applied black to Fortran2003.py
LonelyCat124 Jun 30, 2025
b89e33b
Merge branch 'master' into 468_directives
arporter Jul 29, 2025
06280aa
Changes for review
LonelyCat124 Aug 6, 2025
0bd5e99
Merge branch '468_directives' of github.com:stfc/fparser into 468_dir…
LonelyCat124 Aug 6, 2025
cb37dec
Merge branch 'master' into 468_directives
LonelyCat124 Aug 6, 2025
c1ea3fb
formatting
LonelyCat124 Aug 6, 2025
a77f658
Merge branch '468_directives' of github.com:stfc/fparser into 468_dir…
LonelyCat124 Aug 6, 2025
ab39d9b
Changes for review
LonelyCat124 Aug 29, 2025
cd692dc
Formatting fixes
LonelyCat124 Aug 29, 2025
5b13ff4
Applied black to F2003
LonelyCat124 Aug 29, 2025
927693e
Merge branch 'master' into 468_directives
LonelyCat124 Aug 29, 2025
445ce3e
Changes for review
LonelyCat124 Sep 4, 2025
5cca730
Fix issues with always processing directives
LonelyCat124 Sep 4, 2025
39adf57
Example of usage
LonelyCat124 Sep 4, 2025
9822fd7
Changes for review
LonelyCat124 Sep 5, 2025
12195ad
Missing comment
LonelyCat124 Sep 5, 2025
0be460f
changes for review
LonelyCat124 Sep 8, 2025
ef9ae0f
#468 fix Black formatting
arporter Sep 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/source/fparser2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,23 @@ file was found but would fail if the include file was not found::
program x
include 'endprogram.inc'

Compiler/OpenMP Directive support
---------------------------------
Most Fortran compilers support directives to enable compiler-specific
functionality. Fparser has an option to support converting these into
``Directive`` nodes where possible. This option is ``process_directives``,
and by default it is set to ``False``. If its set to true, it forces
``ignore_comments`` to be ``False``.

The supported directives are those recognized by flang, ifx, ifort (``!dir$``),
and gcc (``!gcc$``), as well as OpenMP directives (such as ``!$omp``
or alternatives).

For example::

reader = FortranFileReader("compute_mod.f90", process_directives=True)


Preprocessing Directives
------------------------

Expand Down
7 changes: 7 additions & 0 deletions src/fparser/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def get_reader(
include_dirs=None,
source_only=None,
ignore_comments=True,
process_directives: bool = False,
):
"""
Returns Fortran reader instance.
Expand All @@ -106,6 +107,10 @@ def get_reader(
encountered.
:param bool ignore_comments: Whether or not to ignore (and discard)
comments when parsing the source.
:param process_directives: whether or not to process directives as
specialised Directive nodes. Default is False (in which case
directives are left as comments). This option overrides the
ignore_comments input.

:returns: a reader instance
:rtype: :py:class:`fparser.common.readfortran.FortranReader`
Expand Down Expand Up @@ -138,13 +143,15 @@ def get_reader(
include_dirs=include_dirs,
source_only=source_only,
ignore_comments=ignore_comments,
process_directives=process_directives,
)
elif isinstance(source, str):
reader = FortranStringReader(
source,
include_dirs=include_dirs,
source_only=source_only,
ignore_comments=ignore_comments,
process_directives=process_directives,
)
else:
raise TypeError("Expected string or filename input but got %s" % (type(input)))
Expand Down
37 changes: 31 additions & 6 deletions src/fparser/common/readfortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,28 +548,37 @@ class FortranReaderBase:
code, free format Fortran code, or PYF signatures (with extended
free format Fortran syntax).

:param source: a file-like object with .next() method used to \
:param source: a file-like object with .next() method used to
retrive a line.
:type source: :py:class:`StringIO` or a file handle
:param mode: a FortranFormat object as returned by \
:param mode: a FortranFormat object as returned by
`sourceinfo.get_source_info()`
:type mode: :py:class:`fparser.common.sourceinfo.Format`
:param bool isstrict: whether we are strictly enforcing fixed format.
:param bool ignore_comments: whether or not to discard comments. If
:param isstrict: whether we are strictly enforcing fixed format.
:param ignore_comments: whether or not to discard comments. If
comments are not ignored, they will be added as special Comment node
to the tree, and will therefore also be added to the output Fortran
source code.
:param Optional[bool] include_omp_conditional_lines: whether or not the
:param include_omp_conditional_lines: whether or not the
content of a line with an OMP sentinel is parsed or not. Default is
False (in which case it is treated as a Comment).
:param process_directives: whether or not to process directives as
specialised Directive nodes. Default is False (in which case
directives are left as comments). This option overrides the
ignore_comments input.

The Fortran source is iterated by `get_single_line`,
`get_next_line`, `put_single_line` methods.

"""

def __init__(
self, source, mode, ignore_comments, include_omp_conditional_lines=False
self,
source,
mode: bool,
ignore_comments: bool,
include_omp_conditional_lines: bool = False,
process_directives: bool = False,
):
self.source = source
self._include_omp_conditional_lines = include_omp_conditional_lines
Expand All @@ -579,6 +588,10 @@ def __init__(
# This value for ignore_comments can be overridden by using the
# ignore_comments optional argument to e.g. get_single_line()
self._ignore_comments = ignore_comments
# Enabling process directives forces comments to be processed.
if process_directives:
self._ignore_comments = False
self.process_directives = process_directives

self.filo_line = [] # used for un-consuming lines.
self.fifo_item = []
Expand Down Expand Up @@ -1680,6 +1693,10 @@ class FortranFileReader(FortranReaderBase):
:param Optional[bool] include_omp_conditional_lines: whether or not the
content of a line with an OMP sentinel is parsed or not. Default is
False (in which case it is treated as a Comment).
:param process_directives: whether or not to process directives as
specialised Directive nodes. Default is False (in which case
directives are left as comments). This option overrides the
ignore_comments input.

For example::

Expand All @@ -1697,6 +1714,7 @@ def __init__(
ignore_comments=True,
ignore_encoding=True,
include_omp_conditional_lines=False,
process_directives: bool = False,
):
# The filename is used as a unique ID. This is then used to cache the
# contents of the file. Obviously if the file changes content but not
Expand Down Expand Up @@ -1729,6 +1747,7 @@ def __init__(
mode,
ignore_comments,
include_omp_conditional_lines=include_omp_conditional_lines,
process_directives=process_directives,
)

if include_dirs is None:
Expand Down Expand Up @@ -1764,6 +1783,10 @@ class FortranStringReader(FortranReaderBase):
:param Optional[bool] include_omp_conditional_lines: whether or not
the content of a line with an OMP sentinel is parsed or not. Default
is False (in which case it is treated as a Comment).
:param process_directives: whether or not to process directives as
specialised Directive nodes. Default is False (in which case
directives are left as comments). This option overrides the
ignore_comments input.

For example:

Expand All @@ -1786,6 +1809,7 @@ def __init__(
ignore_comments=True,
ignore_encoding=True,
include_omp_conditional_lines=False,
process_directives: bool = False,
):
# The Python ID of the string was used to uniquely identify it for
# caching purposes. Unfortunately this ID is only unique for the
Expand All @@ -1806,6 +1830,7 @@ def __init__(
mode,
ignore_comments,
include_omp_conditional_lines=include_omp_conditional_lines,
process_directives=process_directives,
)
if include_dirs is not None:
self.include_dirs = include_dirs[:]
Expand Down
19 changes: 19 additions & 0 deletions src/fparser/common/tests/test_readfortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,3 +1730,22 @@ def test_conditional_include_omp_conditional_liness_free_format_multiple():
line = reader.next()
# 8 spaces in input_text, plus two for replacing the !$
assert line.line == "bla bla"


def test_process_directives_option_read_fortran():
"""Test handling of the process_directives option."""

input_text = "!$omp target\n! comment"
reader = FortranStringReader(input_text)
assert not reader.process_directives
assert reader._ignore_comments

reader = FortranStringReader(input_text, process_directives=True)
assert reader.process_directives
assert not reader._ignore_comments

reader = FortranStringReader(
input_text, ignore_comments=False, process_directives=True
)
assert reader.process_directives
assert not reader._ignore_comments
129 changes: 111 additions & 18 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
import re
import sys

from typing import Union

from fparser.common.splitline import string_replace_map
from fparser.two import pattern_tools as pattern
from fparser.common.readfortran import FortranReaderBase
Expand Down Expand Up @@ -116,9 +118,104 @@
# R102: <xyz-name> = <name>
# R103: <scalar-xyz> = <xyz>


#
# SECTION 2
#
class Directive(Base):
"""
Represents a Directive. Directives are leaves in the tree, containing
a single item consisting of the directive string.

Fparser supports the following directive formats:

1. '!$dir' for generic directives.
2. '!dir$' for the flang, ifx or ifort compilers.
3. '!gcc$' for the gfortran compiler.
4. '!$omp', '!$ompx', 'c$omp', '*$omp', '!$omx', 'c$omx', and '*$omx' for
OpenMP directives.
"""

subclass_names = []
_directive_formats = [
"!$dir", # Generic directive
"!dir$", # flang, ifx, ifort directives.
"cdir$", # flang, ifx, ifort fixed format directive.
"!$omp", # OpenMP directive
"c$omp", # OpenMP fixed format directive
"*$omp", # OpenMP fixed format directive
"!$omx", # OpenMP fixed format directive
"c$omx", # OpenMP fixed format directive
"*$omx", # OpenMP fixed format directive
"!gcc$", # GCC compiler directive
"!$ompx", # OpenMP extension directive
]

@show_result
def __new__(cls, string: Union[str, FortranReaderBase], parent_cls=None):
"""
Create a new Directive instance.

:param type cls: the class of object to create.
:param string: (source of) Fortran string to parse.
:param parent_cls: the parent class of this object.
:type parent_cls: :py:type:`type`

"""
from fparser.common import readfortran

if isinstance(string, readfortran.Comment):
# Directives must start with one of the specified directive
# prefixes.
lower = string.comment.lower()
if not (
any(
[
lower.startswith(prefix)
for prefix in Directive._directive_formats
]
)
):
return
# We were after a directive and we got a directive. Construct
# one manually to avoid recursively calling this __new__
# method again...
obj = object.__new__(cls)
obj.init(string)
return obj
if isinstance(string, FortranReaderBase):
reader = string
item = reader.get_item()
if item is None:
return
if isinstance(item, readfortran.Comment):
# This effectively recursively calls this routine
res = Directive(item)
if not res:
# We didn't get a directive so put the item back in
# the FIFO
reader.put_item(item)
return res
# We didn't get a directive so put the item back in the FIFO
reader.put_item(item)
# We didn't get a directive
return

def init(self, comment) -> None:
"""
Initialise this Directive from a comment object.

:param comment: The comment object produced by the reader
:type comment: :py:class:`readfortran.Comment`
"""
self.items = [comment.comment]
self.item = comment

def tostr(self) -> str:
"""
:returns: this directive as a string.
"""
return str(self.items[0])


class Comment(Base):
Expand Down Expand Up @@ -182,32 +279,28 @@ def tostr(self):
"""
return str(self.items[0])

def restore_reader(self, reader):
"""
Undo the read of this comment by putting its content back
into the reader (which has a FIFO buffer)

:param reader: the reader instance to return the comment to
:type reader: :py:class:`fparser.readfortran.FortranReaderBase`
"""
reader.put_item(self.item)


def match_comment_or_include(reader):
"""Creates a comment or include object from the current line.
"""Creates a comment, directive, or include object from the current line.

:param reader: the fortran file reader containing the line \
:param reader: the fortran file reader containing the line
of code that we are trying to match
:type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \
or \
:py:class:`fparser.common.readfortran.FortranStringReader`
:type reader: :py:class:`fparser.common.readfortran.FortranFileReader`
or
:py:class:`fparser.common.readfortran.FortranStringReader`

:return: a comment or include object if found, otherwise `None`.
:rtype: :py:class:`fparser.two.Fortran2003.Comment` or \
:return: a comment, directive, or include object if found, otherwise
`None`.
:rtype: :py:class:`fparser.two.Fortran2003.Comment` or
:py:class:`fparser.two.Fortran2003.Include_Stmt`
or :py:class:`fparser.two.Fortran2003.Directive`

"""
obj = Comment(reader)
obj = None
# Whether or not to specialise Directives is a run-time option.
if reader.process_directives:
obj = Directive(reader)
obj = Comment(reader) if not obj else obj
obj = Include_Stmt(reader) if not obj else obj
return obj

Expand Down
Loading