Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Modifications by (in alphabetical order):
* P. Vitt, University of Siegen, Germany
* A. Voysey, UK Met Office

31/10/2025 PR #486 for #483. Recognize any comment that begins ``!$``, ``c$`` or ``*$`` followed by
a character as a Directive node.

## Release 0.2.1 (29/09/2025) ##

08/09/2025 PR #469 for #468. Added (optional) Directive node separated from comments.

29/08/2025 PR #477 for #476. Add Python 3.9 testing back to support upstream requirements.
Expand Down
5 changes: 3 additions & 2 deletions doc/source/fparser2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,9 @@ 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).
and gcc (``!gcc$``), as well as support for any generic directive. A generic
directive is any comment that begins ``!$``, ``c$`` or ``*$`` followed by an
alphabetical character.

For example::

Expand Down
27 changes: 10 additions & 17 deletions src/fparser/two/Fortran2003.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,20 @@ class Directive(Base):

Fparser supports the following directive formats:

1. '!$dir' for generic directives.
2. '!dir$' for the flang, ifx or ifort compilers.
1. '!$', 'c$' or '*$' followed by any alphabetical character for
generic directives.
2. '!dir$' or 'cdir$' 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 = []
# TODO #483 - Add OpenACC directive support.
_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
r"\!\$[a-z]", # Generic directive
r"c\$[a-z]", # Generic directive
r"\*\$[a-z]", # Generic directive
r"\!dir\$", # flang, ifx, ifort directives.
r"cdir\$", # flang, ifx, ifort fixed format directive.
r"\!gcc\$", # GCC compiler directive
]

@show_result
Expand All @@ -172,7 +165,7 @@ def __new__(cls, string: Union[str, FortranReaderBase], parent_cls=None):
if not (
any(
[
lower.startswith(prefix)
re.match(prefix, lower) is not None
for prefix in Directive._directive_formats
]
)
Expand Down
126 changes: 98 additions & 28 deletions src/fparser/two/tests/test_comments_and_directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def test_prog_comments():
)

obj = cls(reader)
assert type(obj) == Program
assert type(obj) is Program
# Check that the AST has the expected structure:
# Program
# |--> Comment
Expand All @@ -254,21 +254,21 @@ def test_prog_comments():
from fparser.two.Fortran2003 import Main_Program, Write_Stmt, End_Program_Stmt

walk(obj.children, Comment, debug=True)
assert type(obj.content[0]) == Comment
assert type(obj.content[0]) is Comment
assert str(obj.content[0]) == "! A troublesome comment"
assert type(obj.content[1]) == Main_Program
assert type(obj.content[1]) is Main_Program
main_prog = obj.content[1]
assert type(main_prog.content[1].content[0].content[0]) == Comment
assert type(main_prog.content[1].content[0].content[0]) is Comment
assert str(main_prog.content[1].content[0].content[0]) == "! A full comment line"
exec_part = main_prog.content[2]
assert type(exec_part.content[0]) == Write_Stmt
assert type(exec_part.content[0]) is Write_Stmt
# Check that we have the in-line comment as a second statement
assert len(exec_part.content) == 2
assert type(exec_part.content[1]) == Comment
assert type(main_prog.content[3]) == End_Program_Stmt
assert type(exec_part.content[1]) is Comment
assert type(main_prog.content[3]) is End_Program_Stmt
assert "! An in-line comment" in str(obj)
# Check that we still have the ending comment
assert type(obj.content[-1]) == Comment
assert type(obj.content[-1]) is Comment
assert str(obj).endswith("! A really problematic comment")


Expand All @@ -283,7 +283,7 @@ def test_module_comments():
# Test when the reader is explicitly set to free-form mode
reader = get_reader(source, isfree=True, ignore_comments=False)
prog_unit = Program(reader)
assert type(prog_unit.content[0]) == Comment
assert type(prog_unit.content[0]) is Comment
assert str(prog_unit.content[0]) == "! This is a module"


Expand Down Expand Up @@ -441,7 +441,7 @@ def test_directive_stmts():

# Check the restore_reader works correctly for directive.
old = reader.get_item()
assert old == None
assert old is None
out[2].restore_reader(reader)
old = reader.get_item()
assert old is not None
Expand Down Expand Up @@ -481,26 +481,35 @@ def test_directive_stmts():
@pytest.mark.parametrize(
"directive,expected,free",
[
("!$dir always", "!$dir always", True),
("!dir$ always", "!dir$ always", True),
("!gcc$ vector", "!gcc$ vector", True),
("!$omp parallel", "!$omp parallel", True),
("!$ompx parallel", "!$ompx parallel", True),
("c$omp parallel", "c$omp parallel", False),
("c$omx parallel", "c$omx parallel", False),
("!$omx parallel", "!$omx parallel", False),
("*$omp parallel", "*$omp parallel", False),
("c$omx parallel", "c$omx parallel", False),
("*$omx parallel", "*$omx parallel", False),
("!$dir always", ("!$dir always",), True),
("!dir$ always", ("!dir$ always",), True),
("!gcc$ vector", ("!gcc$ vector",), True),
("!$acc loop", ("!$acc loop",), True),
("!$omp parallel", ("!$omp parallel",), True),
("!$ompx parallel", ("!$ompx parallel",), True),
("c$omp parallel", ("c$omp parallel",), False),
("c$omx parallel", ("c$omx parallel",), False),
("!$omx parallel", ("!$omx parallel",), False),
("*$omp parallel", ("*$omp parallel",), False),
("c$omx parallel", ("c$omx parallel",), False),
("*$omx parallel", ("*$omx parallel",), False),
("!$DIR ALWAYS", ("!$DIR ALWAYS",), True),
("c$OMX PARALLEL", ("c$OMX PARALLEL",), False),
("!$omp parallel&\n!$omp&do", ("!$omp parallel&", "!$omp&do"), True),
(
"c$omp parallel do\nc$omp+shared(a,b,c)",
("c$omp parallel do", "c$omp+shared(a,b,c)"),
False,
),
("!!$omp parallel", (), True),
],
)
def test_all_directive_formats(directive, expected, free):
"""Parameterized test to ensure that all directive formats are
correctly recognized."""
# Tests for free-form directives
# Generate the source code
if free:
source = """
Program my_prog
source = """Program my_prog
integer :: x
"""
source = source + directive + "\n"
Expand All @@ -522,12 +531,73 @@ def test_all_directive_formats(directive, expected, free):
)
program = Program(reader)
out = walk(program, Directive)
assert len(out) == 1
assert out[0].items[0] == expected
assert len(out) == len(expected)
for i, direc in enumerate(out):
assert direc.items[0] == expected[i]

# Test that we correctly get directives without ignore_comments=False.
reader = get_reader(source, isfree=free, process_directives=True)
program = Program(reader)
out = walk(program, Directive)
assert len(out) == 1
assert out[0].items[0] == expected
assert len(out) == len(expected)
for i, direc in enumerate(out):
assert direc.items[0] == expected[i]


@pytest.mark.parametrize(
"directive,expected,free",
[
("!$dir always", ("!$dir always",), True),
("!dir$ always", ("!dir$ always",), True),
("!gcc$ vector", ("!gcc$ vector",), True),
("!$omp parallel", ("!$omp parallel",), True),
("!$ompx parallel", ("!$ompx parallel",), True),
("c$omp parallel", ("c$omp parallel",), False),
("c$omx parallel", ("c$omx parallel",), False),
("!$omx parallel", ("!$omx parallel",), False),
("*$omp parallel", ("*$omp parallel",), False),
("c$omx parallel", ("c$omx parallel",), False),
("*$omx parallel", ("*$omx parallel",), False),
("!$DIR ALWAYS", ("!$DIR ALWAYS",), True),
("c$OMX PARALLEL", ("c$OMX PARALLEL",), False),
("!$omp parallel&\n!$omp&do", ("!$omp parallel&", "!$omp&do"), True),
(
"c$omp parallel do\nc$omp+shared(a,b,c)",
("c$omp parallel do", "c$omp+shared(a,b,c)"),
False,
),
("!!$omp parallel", ("!!$omp parallel",), True),
],
)
def test_directives_as_comments(directive, expected, free):
"""Parameterized test to ensure all directives produce comments when
process_directives is disabled."""
# Generate the source code
if free:
source = """Program my_prog
integer :: x
"""
source = source + directive + "\n"
source = (
source
+ """ do x= 1 , 100
end do
End Program"""
)
else:
source = """\
program foo
"""
source = source + directive + "\n"
source = source + " end program foo"
# Test that we get the expected comments with comments only
reader = get_reader(
source, isfree=free, ignore_comments=False, process_directives=False
)
program = Program(reader)
out = walk(program, Comment)
# Check that we have the correct number of comments.
assert len(out) == len(expected)
# Check that the comments contain the correct strings.
for i, direc in enumerate(out):
assert direc.items[0] == expected[i]