Skip to content

Commit 669e2b1

Browse files
authored
Merge pull request #486 from stfc/483_generic_directives
(Closes #483) Recognize generic directives
2 parents eee1f4f + 37db93d commit 669e2b1

File tree

4 files changed

+116
-47
lines changed

4 files changed

+116
-47
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ Modifications by (in alphabetical order):
2020
* P. Vitt, University of Siegen, Germany
2121
* A. Voysey, UK Met Office
2222

23+
31/10/2025 PR #486 for #483. Recognize any comment that begins ``!$``, ``c$`` or ``*$`` followed by
24+
a character as a Directive node.
25+
26+
## Release 0.2.1 (29/09/2025) ##
27+
2328
08/09/2025 PR #469 for #468. Added (optional) Directive node separated from comments.
2429

2530
29/08/2025 PR #477 for #476. Add Python 3.9 testing back to support upstream requirements.

doc/source/fparser2.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,9 @@ and by default it is set to ``False``. If its set to true, it forces
504504
``ignore_comments`` to be ``False``.
505505

506506
The supported directives are those recognized by flang, ifx, ifort (``!dir$``),
507-
and gcc (``!gcc$``), as well as OpenMP directives (such as ``!$omp``
508-
or alternatives).
507+
and gcc (``!gcc$``), as well as support for any generic directive. A generic
508+
directive is any comment that begins ``!$``, ``c$`` or ``*$`` followed by an
509+
alphabetical character.
509510

510511
For example::
511512

src/fparser/two/Fortran2003.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -129,27 +129,20 @@ class Directive(Base):
129129
130130
Fparser supports the following directive formats:
131131
132-
1. '!$dir' for generic directives.
133-
2. '!dir$' for the flang, ifx or ifort compilers.
132+
1. '!$', 'c$' or '*$' followed by any alphabetical character for
133+
generic directives.
134+
2. '!dir$' or 'cdir$' for the flang, ifx or ifort compilers.
134135
3. '!gcc$' for the gfortran compiler.
135-
4. '!$omp', '!$ompx', 'c$omp', '*$omp', '!$omx', 'c$omx', and '*$omx' for
136-
OpenMP directives.
137136
"""
138137

139138
subclass_names = []
140-
# TODO #483 - Add OpenACC directive support.
141139
_directive_formats = [
142-
"!$dir", # Generic directive
143-
"!dir$", # flang, ifx, ifort directives.
144-
"cdir$", # flang, ifx, ifort fixed format directive.
145-
"!$omp", # OpenMP directive
146-
"c$omp", # OpenMP fixed format directive
147-
"*$omp", # OpenMP fixed format directive
148-
"!$omx", # OpenMP fixed format directive
149-
"c$omx", # OpenMP fixed format directive
150-
"*$omx", # OpenMP fixed format directive
151-
"!gcc$", # GCC compiler directive
152-
"!$ompx", # OpenMP extension directive
140+
r"\!\$[a-z]", # Generic directive
141+
r"c\$[a-z]", # Generic directive
142+
r"\*\$[a-z]", # Generic directive
143+
r"\!dir\$", # flang, ifx, ifort directives.
144+
r"cdir\$", # flang, ifx, ifort fixed format directive.
145+
r"\!gcc\$", # GCC compiler directive
153146
]
154147

155148
@show_result
@@ -172,7 +165,7 @@ def __new__(cls, string: Union[str, FortranReaderBase], parent_cls=None):
172165
if not (
173166
any(
174167
[
175-
lower.startswith(prefix)
168+
re.match(prefix, lower) is not None
176169
for prefix in Directive._directive_formats
177170
]
178171
)

src/fparser/two/tests/test_comments_and_directives.py

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def test_prog_comments():
236236
)
237237

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

256256
walk(obj.children, Comment, debug=True)
257-
assert type(obj.content[0]) == Comment
257+
assert type(obj.content[0]) is Comment
258258
assert str(obj.content[0]) == "! A troublesome comment"
259-
assert type(obj.content[1]) == Main_Program
259+
assert type(obj.content[1]) is Main_Program
260260
main_prog = obj.content[1]
261-
assert type(main_prog.content[1].content[0].content[0]) == Comment
261+
assert type(main_prog.content[1].content[0].content[0]) is Comment
262262
assert str(main_prog.content[1].content[0].content[0]) == "! A full comment line"
263263
exec_part = main_prog.content[2]
264-
assert type(exec_part.content[0]) == Write_Stmt
264+
assert type(exec_part.content[0]) is Write_Stmt
265265
# Check that we have the in-line comment as a second statement
266266
assert len(exec_part.content) == 2
267-
assert type(exec_part.content[1]) == Comment
268-
assert type(main_prog.content[3]) == End_Program_Stmt
267+
assert type(exec_part.content[1]) is Comment
268+
assert type(main_prog.content[3]) is End_Program_Stmt
269269
assert "! An in-line comment" in str(obj)
270270
# Check that we still have the ending comment
271-
assert type(obj.content[-1]) == Comment
271+
assert type(obj.content[-1]) is Comment
272272
assert str(obj).endswith("! A really problematic comment")
273273

274274

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

289289

@@ -441,7 +441,7 @@ def test_directive_stmts():
441441

442442
# Check the restore_reader works correctly for directive.
443443
old = reader.get_item()
444-
assert old == None
444+
assert old is None
445445
out[2].restore_reader(reader)
446446
old = reader.get_item()
447447
assert old is not None
@@ -481,26 +481,35 @@ def test_directive_stmts():
481481
@pytest.mark.parametrize(
482482
"directive,expected,free",
483483
[
484-
("!$dir always", "!$dir always", True),
485-
("!dir$ always", "!dir$ always", True),
486-
("!gcc$ vector", "!gcc$ vector", True),
487-
("!$omp parallel", "!$omp parallel", True),
488-
("!$ompx parallel", "!$ompx parallel", True),
489-
("c$omp parallel", "c$omp parallel", False),
490-
("c$omx parallel", "c$omx parallel", False),
491-
("!$omx parallel", "!$omx parallel", False),
492-
("*$omp parallel", "*$omp parallel", False),
493-
("c$omx parallel", "c$omx parallel", False),
494-
("*$omx parallel", "*$omx parallel", False),
484+
("!$dir always", ("!$dir always",), True),
485+
("!dir$ always", ("!dir$ always",), True),
486+
("!gcc$ vector", ("!gcc$ vector",), True),
487+
("!$acc loop", ("!$acc loop",), True),
488+
("!$omp parallel", ("!$omp parallel",), True),
489+
("!$ompx parallel", ("!$ompx parallel",), True),
490+
("c$omp parallel", ("c$omp parallel",), False),
491+
("c$omx parallel", ("c$omx parallel",), False),
492+
("!$omx parallel", ("!$omx parallel",), False),
493+
("*$omp parallel", ("*$omp parallel",), False),
494+
("c$omx parallel", ("c$omx parallel",), False),
495+
("*$omx parallel", ("*$omx parallel",), False),
496+
("!$DIR ALWAYS", ("!$DIR ALWAYS",), True),
497+
("c$OMX PARALLEL", ("c$OMX PARALLEL",), False),
498+
("!$omp parallel&\n!$omp&do", ("!$omp parallel&", "!$omp&do"), True),
499+
(
500+
"c$omp parallel do\nc$omp+shared(a,b,c)",
501+
("c$omp parallel do", "c$omp+shared(a,b,c)"),
502+
False,
503+
),
504+
("!!$omp parallel", (), True),
495505
],
496506
)
497507
def test_all_directive_formats(directive, expected, free):
498508
"""Parameterized test to ensure that all directive formats are
499509
correctly recognized."""
500-
# Tests for free-form directives
510+
# Generate the source code
501511
if free:
502-
source = """
503-
Program my_prog
512+
source = """Program my_prog
504513
integer :: x
505514
"""
506515
source = source + directive + "\n"
@@ -522,12 +531,73 @@ def test_all_directive_formats(directive, expected, free):
522531
)
523532
program = Program(reader)
524533
out = walk(program, Directive)
525-
assert len(out) == 1
526-
assert out[0].items[0] == expected
534+
assert len(out) == len(expected)
535+
for i, direc in enumerate(out):
536+
assert direc.items[0] == expected[i]
527537

528538
# Test that we correctly get directives without ignore_comments=False.
529539
reader = get_reader(source, isfree=free, process_directives=True)
530540
program = Program(reader)
531541
out = walk(program, Directive)
532-
assert len(out) == 1
533-
assert out[0].items[0] == expected
542+
assert len(out) == len(expected)
543+
for i, direc in enumerate(out):
544+
assert direc.items[0] == expected[i]
545+
546+
547+
@pytest.mark.parametrize(
548+
"directive,expected,free",
549+
[
550+
("!$dir always", ("!$dir always",), True),
551+
("!dir$ always", ("!dir$ always",), True),
552+
("!gcc$ vector", ("!gcc$ vector",), True),
553+
("!$omp parallel", ("!$omp parallel",), True),
554+
("!$ompx parallel", ("!$ompx parallel",), True),
555+
("c$omp parallel", ("c$omp parallel",), False),
556+
("c$omx parallel", ("c$omx parallel",), False),
557+
("!$omx parallel", ("!$omx parallel",), False),
558+
("*$omp parallel", ("*$omp parallel",), False),
559+
("c$omx parallel", ("c$omx parallel",), False),
560+
("*$omx parallel", ("*$omx parallel",), False),
561+
("!$DIR ALWAYS", ("!$DIR ALWAYS",), True),
562+
("c$OMX PARALLEL", ("c$OMX PARALLEL",), False),
563+
("!$omp parallel&\n!$omp&do", ("!$omp parallel&", "!$omp&do"), True),
564+
(
565+
"c$omp parallel do\nc$omp+shared(a,b,c)",
566+
("c$omp parallel do", "c$omp+shared(a,b,c)"),
567+
False,
568+
),
569+
("!!$omp parallel", ("!!$omp parallel",), True),
570+
],
571+
)
572+
def test_directives_as_comments(directive, expected, free):
573+
"""Parameterized test to ensure all directives produce comments when
574+
process_directives is disabled."""
575+
# Generate the source code
576+
if free:
577+
source = """Program my_prog
578+
integer :: x
579+
"""
580+
source = source + directive + "\n"
581+
source = (
582+
source
583+
+ """ do x= 1 , 100
584+
end do
585+
End Program"""
586+
)
587+
else:
588+
source = """\
589+
program foo
590+
"""
591+
source = source + directive + "\n"
592+
source = source + " end program foo"
593+
# Test that we get the expected comments with comments only
594+
reader = get_reader(
595+
source, isfree=free, ignore_comments=False, process_directives=False
596+
)
597+
program = Program(reader)
598+
out = walk(program, Comment)
599+
# Check that we have the correct number of comments.
600+
assert len(out) == len(expected)
601+
# Check that the comments contain the correct strings.
602+
for i, direc in enumerate(out):
603+
assert direc.items[0] == expected[i]

0 commit comments

Comments
 (0)