diff --git a/CHANGELOG.md b/CHANGELOG.md index 372a6d87..80fbcf8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/doc/source/fparser2.rst b/doc/source/fparser2.rst index d3b45f7b..a7571db7 100644 --- a/doc/source/fparser2.rst +++ b/doc/source/fparser2.rst @@ -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:: diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 6497dac4..fa7f342a 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -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 @@ -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 ] ) diff --git a/src/fparser/two/tests/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index 120a4444..59017f4e 100644 --- a/src/fparser/two/tests/test_comments_and_directives.py +++ b/src/fparser/two/tests/test_comments_and_directives.py @@ -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 @@ -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") @@ -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" @@ -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 @@ -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" @@ -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]