Skip to content

Commit

Permalink
297 temporary omp sentinel support (MetOffice#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
hiker authored Jul 18, 2024
1 parent 4157606 commit 77cb175
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 20 deletions.
28 changes: 26 additions & 2 deletions source/fab/parse/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pathlib import Path
from typing import Union, Optional, Iterable, Dict, Any, Set

from fparser.common.readfortran import FortranStringReader # type: ignore
from fparser.two.Fortran2003 import ( # type: ignore
Entity_Decl_List, Use_Stmt, Module_Stmt, Program_Stmt, Subroutine_Stmt, Function_Stmt, Language_Binding_Spec,
Char_Literal_Constant, Interface_Block, Name, Comment, Module, Call_Stmt, Derived_Type_Def, Derived_Type_Stmt,
Expand Down Expand Up @@ -284,15 +285,38 @@ def _process_comment(self, analysed_file, obj):
# TODO: error handling in case we catch a genuine comment
# TODO: separate this project-specific code from the generic f analyser?
depends_str = "DEPENDS ON:"
if depends_str in obj.items[0]:
comment = obj.items[0].strip()
if depends_str in comment:
self.depends_on_comment_found = True
dep = obj.items[0].split(depends_str)[-1].strip()
dep = comment.split(depends_str)[-1].strip()
# with .o means a c file
if dep.endswith(".o"):
analysed_file.mo_commented_file_deps.add(dep.replace(".o", ".c"))
# without .o means a fortran symbol
else:
analysed_file.add_symbol_dep(dep)
if comment[:2] == "!$":
# Check if it is a use statement with an OpenMP sentinel:
# Use fparser's string reader to discard potential comment
# TODO #13: once fparser supports reading the sentinels,
# this can be removed.
reader = FortranStringReader(comment[2:])
line = reader.next()
try:
# match returns a 5-tuple, the third one being the module name
module_name = Use_Stmt.match(line.strline)[2]
module_name = module_name.string
except Exception:
# Not a use statement in a sentinel, ignore:
return

# Register the module name
if module_name in self.ignore_mod_deps:
logger.debug(f"ignoring use of {module_name}")
return
if module_name.lower() not in self._intrinsic_modules:
# found a dependency on fortran
analysed_file.add_module_dep(module_name)

def _process_subroutine_or_function(self, analysed_file, fpath, obj):
# binding?
Expand Down
9 changes: 9 additions & 0 deletions tests/unit_tests/parse/fortran/test_fortran_analyser.f90
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ SUBROUTINE internal_sub
RETURN
END SUBROUTINE internal_sub

SUBROUTINE openmp_sentinel
!$ USE compute_chunk_size_mod, ONLY: compute_chunk_size ! Note OpenMP sentinel
!$ USE test that is not a sentinel with a use statement inside
!GCC$ unroll 6
!DIR$ assume (mod(p, 6) == 0)
!$omp do
!$acc parallel copyin (array, scalar).
END SUBROUTINE openmp_sentinel

INTEGER FUNCTION internal_func()
internal_func = 456
END FUNCTION internal_func
Expand Down
53 changes: 35 additions & 18 deletions tests/unit_tests/parse/fortran/test_fortran_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,35 @@
from fab.parse.fortran_common import iter_content
from fab.tools import ToolBox

'''Tests the Fortran analyser.
'''

# todo: test function binding


@pytest.fixture
def module_fpath():
def module_fpath() -> Path:
'''Simple fixture that sets the name of the module test file.'''
return Path(__file__).parent / "test_fortran_analyser.f90"


@pytest.fixture
def module_expected(module_fpath):
def module_expected(module_fpath) -> AnalysedFortran:
'''Returns the expected AnalysedFortran instance for the Fortran
test module.'''
return AnalysedFortran(
fpath=module_fpath,
file_hash=4039845747,
file_hash=1757501304,
module_defs={'foo_mod'},
symbol_defs={'external_sub', 'external_func', 'foo_mod'},
module_deps={'bar_mod'},
symbol_deps={'monty_func', 'bar_mod'},
module_deps={'bar_mod', 'compute_chunk_size_mod'},
symbol_deps={'monty_func', 'bar_mod', 'compute_chunk_size_mod'},
file_deps=set(),
mo_commented_file_deps={'some_file.c'},
)


class Test_Analyser(object):
class TestAnalyser:

@pytest.fixture
def fortran_analyser(self, tmp_path):
Expand All @@ -53,29 +58,37 @@ def fortran_analyser(self, tmp_path):
def test_empty_file(self, fortran_analyser):
# make sure we get back an EmptySourceFile
with mock.patch('fab.parse.AnalysedFile.save'):
analysis, artefact = fortran_analyser.run(fpath=Path(Path(__file__).parent / "empty.f90"))
assert type(analysis) is EmptySourceFile
analysis, artefact = fortran_analyser.run(
fpath=Path(Path(__file__).parent / "empty.f90"))
assert isinstance(analysis, EmptySourceFile)
assert artefact is None

def test_module_file(self, fortran_analyser, module_fpath, module_expected):
def test_module_file(self, fortran_analyser, module_fpath,
module_expected):
with mock.patch('fab.parse.AnalysedFile.save'):
analysis, artefact = fortran_analyser.run(fpath=module_fpath)
assert analysis == module_expected
assert artefact == fortran_analyser._config.prebuild_folder / f'test_fortran_analyser.{analysis.file_hash}.an'
assert artefact == (fortran_analyser._config.prebuild_folder /
f'test_fortran_analyser.{analysis.file_hash}.an')

def test_program_file(self, fortran_analyser, module_fpath, module_expected):
def test_program_file(self, fortran_analyser, module_fpath,
module_expected):
# same as test_module_file() but replacing MODULE with PROGRAM
with NamedTemporaryFile(mode='w+t', suffix='.f90') as tmp_file:
tmp_file.write(module_fpath.open().read().replace("MODULE", "PROGRAM"))
tmp_file.write(module_fpath.open().read().replace("MODULE",
"PROGRAM"))
tmp_file.flush()
with mock.patch('fab.parse.AnalysedFile.save'):
analysis, artefact = fortran_analyser.run(fpath=Path(tmp_file.name))
analysis, artefact = fortran_analyser.run(
fpath=Path(tmp_file.name))

module_expected.fpath = Path(tmp_file.name)
module_expected._file_hash = 768896775
module_expected._file_hash = 3388519280
module_expected.program_defs = {'foo_mod'}
module_expected.module_defs = set()
module_expected.symbol_defs.update({'internal_sub', 'internal_func'})
module_expected.symbol_defs.update({'internal_func',
'internal_sub',
'openmp_sentinel'})

assert analysis == module_expected
assert artefact == fortran_analyser._config.prebuild_folder \
Expand All @@ -84,11 +97,13 @@ def test_program_file(self, fortran_analyser, module_fpath, module_expected):

# todo: test more methods!

class Test_process_variable_binding(object):
class TestProcessVariableBinding:
'''This test class tests the variable binding.'''

# todo: define and depend, with and without bind name

def test_define_without_bind_name(self, tmp_path):
'''Test usage of bind'''
fpath = tmp_path / 'temp.f90'

open(fpath, 'wt').write("""
Expand All @@ -112,13 +127,15 @@ def test_define_without_bind_name(self, tmp_path):
tree = f2008_parser(reader)

# find the tree node representing the variable binding
var_decl = next(obj for obj in iter_content(tree) if isinstance(obj, Type_Declaration_Stmt))
var_decl = next(obj for obj in iter_content(tree)
if isinstance(obj, Type_Declaration_Stmt))

# run our handler
fpath = Path('foo')
analysed_file = AnalysedFortran(fpath=fpath, file_hash=0)
analyser = FortranAnalyser()
analyser._process_variable_binding(analysed_file=analysed_file, obj=var_decl)
analyser._process_variable_binding(analysed_file=analysed_file,
obj=var_decl)

assert analysed_file.symbol_defs == {'helloworld', }

Expand Down

0 comments on commit 77cb175

Please sign in to comment.