From 671d2060ca5c30d46280cd2902dc2b5b6ea93b6d Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Fri, 1 Sep 2023 13:37:04 +0100 Subject: [PATCH 1/6] Apply trafo in Scheduler.process directly to IR node --- loki/bulk/scheduler.py | 25 +++- loki/transform/dependency_transform.py | 7 +- loki/transform/transformation.py | 4 - scripts/loki_transform.py | 6 +- tests/test_scheduler.py | 157 ++++++++++++++++++++++--- 5 files changed, 172 insertions(+), 27 deletions(-) diff --git a/loki/bulk/scheduler.py b/loki/bulk/scheduler.py index 5127898de..e7d8d93ac 100644 --- a/loki/bulk/scheduler.py +++ b/loki/bulk/scheduler.py @@ -599,7 +599,8 @@ def item_successors(self, item): successors += [self.item_map[child.name]] + self.item_successors(child) return successors - def process(self, transformation, reverse=False, item_filter=SubroutineItem, use_file_graph=False): + def process(self, transformation, reverse=False, item_filter=SubroutineItem, use_file_graph=False, + recurse_to_contained_nodes=False): """ Process all :attr:`items` in the scheduler's graph @@ -612,6 +613,12 @@ def process(self, transformation, reverse=False, item_filter=SubroutineItem, use by setting :data:`use_file_graph` to ``True``. Currently, this calls the transformation on the first `item` associated with a file only. In this mode, :data:`item_filter` does not have any effect. + + The scheduler applies the transformation to the IR node corresponding to + each item in the scheduler's graph. Optionally, the transformation can also + be applied recursively, e.g., to all member subroutines contained in a + subroutine object, or all modules and subroutines contained in a source file, + by setting :data:`recurse_to_contained_nodes` to ``True``. """ trafo_name = transformation.__class__.__name__ log = f'[Loki::Scheduler] Applied transformation <{trafo_name}>' + ' in {:.2f}s' @@ -629,7 +636,10 @@ def process(self, transformation, reverse=False, item_filter=SubroutineItem, use if use_file_graph: for node in traversal: items = graph.nodes[node]['items'] - transformation.apply(items[0].source, item=items[0], items=items, recurse_to_contained_nodes=True) + transformation.apply( + items[0].source, item=items[0], items=items, + recurse_to_contained_nodes=recurse_to_contained_nodes + ) else: for item in traversal: if item_filter and not isinstance(item, item_filter): @@ -638,12 +648,19 @@ def process(self, transformation, reverse=False, item_filter=SubroutineItem, use # Use entry from item_map to ensure the original item is used in transformation _item = self.item_map[item.name] + # Pick out the IR node to which to apply the transformation + # TODO: should this become an Item property? + if isinstance(item, SubroutineItem): + source = _item.routine + else: + source = _item.scope + # Process work item with appropriate kernel transformation.apply( - _item.source, role=_item.role, mode=_item.mode, + source, role=_item.role, mode=_item.mode, item=_item, targets=_item.targets, successors=self.item_successors(_item), depths=self.depths, - recurse_to_contained_nodes=True + recurse_to_contained_nodes=recurse_to_contained_nodes ) def callgraph(self, path, with_file_graph=False): diff --git a/loki/transform/dependency_transform.py b/loki/transform/dependency_transform.py index d3feaf879..e98c2c581 100644 --- a/loki/transform/dependency_transform.py +++ b/loki/transform/dependency_transform.py @@ -80,6 +80,11 @@ def transform_subroutine(self, routine, **kwargs): role = kwargs.get('role') if role == 'kernel': + if routine.name.endswith(self.suffix): + # This is to ensure that the transformation is idempotent if + # applied more than once to a routine + return + # Change the name of kernel routines if routine.is_function: if not routine.result_name: @@ -305,7 +310,7 @@ def module_wrap(self, sourcefile, **kwargs): for routine in sourcefile.subroutines: if routine not in module_routines: # Skip member functions - if item and f'#{routine.name.lower()}' != item.name.lower(): + if item and routine.name.lower() != item.local_name.lower(): continue # Skip internal utility routines too diff --git a/loki/transform/transformation.py b/loki/transform/transformation.py index b1877c5cd..6ab00fb78 100644 --- a/loki/transform/transformation.py +++ b/loki/transform/transformation.py @@ -173,10 +173,6 @@ def apply_subroutine(self, subroutine, recurse_to_contained_nodes=False, **kwarg if subroutine._incomplete: raise RuntimeError('Transformation.apply_subroutine requires Subroutine to be complete') - # Bail if the subroutine has not actually been scheduled for processing - if (item := kwargs.get('item', None)) and item.local_name != subroutine.name.lower(): - return - # Apply the actual transformation for subroutines self.transform_subroutine(subroutine, **kwargs) diff --git a/scripts/loki_transform.py b/scripts/loki_transform.py index 53bd2455d..92f9158f7 100644 --- a/scripts/loki_transform.py +++ b/scripts/loki_transform.py @@ -300,7 +300,7 @@ def transform_subroutine(self, routine, **kwargs): mode = mode.replace('-', '_') # Sanitize mode string dependency = DependencyTransformation(suffix=f'_{mode.upper()}', mode='module', module_suffix='_MOD') - scheduler.process(transformation=dependency) + scheduler.process(transformation=dependency, use_file_graph=True, recurse_to_contained_nodes=True) # Write out all modified source files into the build directory scheduler.process(transformation=FileWriteTransformation(builddir=out_path, mode=mode, cuf='cuf' in mode), @@ -538,10 +538,10 @@ def ecphys(mode, config, header, source, build, cpp, directive, frontend): dependency = DependencyTransformation( mode='module', module_suffix='_MOD', suffix=f'_{mode.upper()}' ) - scheduler.process(transformation=dependency) + scheduler.process(transformation=dependency, use_file_graph=True, recurse_to_contained_nodes=True) # Write out all modified source files into the build directory - scheduler.process(transformation=FileWriteTransformation(builddir=build, mode=mode)) + scheduler.process(transformation=FileWriteTransformation(builddir=build, mode=mode), use_file_graph=True) if __name__ == "__main__": diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index eb3537c82..6c7beade6 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -60,7 +60,7 @@ fexprgen, Transformation, BasicType, CMakePlanner, Subroutine, SubroutineItem, ProcedureBindingItem, gettempdir, ProcedureSymbol, ProcedureType, DerivedType, TypeDef, Scalar, Array, FindInlineCalls, - Import, Variable, GenericImportItem, GlobalVarImportItem + Import, Variable, GenericImportItem, GlobalVarImportItem, flatten ) @@ -1389,10 +1389,10 @@ def test_scheduler_typebound_ignore(here, config, frontend): cg_path.with_suffix('.pdf').unlink() -@pytest.mark.parametrize('use_file_graph,reverse', [ - (False, False), (False, True), (True, False), (True, True) -]) -def test_scheduler_traversal_order(here, config, frontend, use_file_graph, reverse): +@pytest.mark.parametrize('use_file_graph', [False, True]) +@pytest.mark.parametrize('reverse', [False, True]) +@pytest.mark.parametrize('recurse_to_contained_nodes', [False, True]) +def test_scheduler_traversal_order(here, config, frontend, use_file_graph, reverse, recurse_to_contained_nodes): """ Test correct traversal order for scheduler processing @@ -1405,13 +1405,32 @@ def test_scheduler_traversal_order(here, config, frontend, use_file_graph, rever ) if use_file_graph: - expected = [ - 'transformation_module_hoist#driver', 'subroutines_mod#kernel1' - ] + if recurse_to_contained_nodes: + # With recursion, we record + expected = [ + [ + 'transformation_module_hoist#driver::driver_mod.f90', + 'transformation_module_hoist#driver::transformation_module_hoist', + 'transformation_module_hoist#driver::driver', + 'transformation_module_hoist#driver::another_driver', + ], [ + 'subroutines_mod#kernel1::subroutines_mod.f90', + 'subroutines_mod#kernel1::subroutines_mod', + 'subroutines_mod#kernel1::kernel1', + 'subroutines_mod#kernel1::kernel2', + 'subroutines_mod#kernel1::device1', + 'subroutines_mod#kernel1::device2', + ] + ] + else: + expected = [ + 'transformation_module_hoist#driver::driver_mod.f90', 'subroutines_mod#kernel1::subroutines_mod.f90' + ] else: expected = [ - 'transformation_module_hoist#driver', 'subroutines_mod#kernel1', 'subroutines_mod#kernel2', - 'subroutines_mod#device1', 'subroutines_mod#device2' + 'transformation_module_hoist#driver::driver', 'subroutines_mod#kernel1::kernel1', + 'subroutines_mod#kernel2::kernel2', 'subroutines_mod#device1::device1', + 'subroutines_mod#device2::device2' ] class LoggingTransformation(Transformation): @@ -1420,16 +1439,124 @@ def __init__(self): self.record = [] def transform_file(self, sourcefile, **kwargs): - item = kwargs['item'] - self.record += [item.name] + self.record += [kwargs['item'].name + '::' + sourcefile.path.name] + + def transform_module(self, module, **kwargs): + self.record += [kwargs['item'].name + '::' + module.name] + + def transform_subroutine(self, routine, **kwargs): + self.record += [kwargs['item'].name + '::' + routine.name] transformation = LoggingTransformation() - scheduler.process(transformation=transformation, reverse=reverse, use_file_graph=use_file_graph) + scheduler.process( + transformation=transformation, reverse=reverse, use_file_graph=use_file_graph, + recurse_to_contained_nodes=recurse_to_contained_nodes + ) if reverse: - assert transformation.record == expected[::-1] + assert transformation.record == flatten(expected[::-1]) + else: + assert transformation.record == flatten(expected) + + +@pytest.mark.parametrize('use_file_graph', [False, True]) +@pytest.mark.parametrize('reverse', [False, True]) +@pytest.mark.parametrize('recurse_to_contained_nodes', [False, True]) +def test_scheduler_member_routines(config, frontend, use_file_graph, reverse, recurse_to_contained_nodes): + """ + Make sure that transformation processing works also for contained member routines + + Notably, this does currently _NOT_ work and this test is here to document that fact and + serve as the test base for when this has been corrected. + """ + fcode_mod = """ +module member_mod + implicit none +contains + subroutine my_routine(ret) + integer, intent(out) :: ret + ret = 1 + end subroutine my_routine + + subroutine driver + integer :: val + call my_member + write(*,*) val + contains + subroutine my_member + call my_routine(val) + end subroutine my_member + end subroutine driver +end module member_mod + """.strip() + + workdir = gettempdir()/'test_scheduler_member_routines' + workdir.mkdir(exist_ok=True) + (workdir/'member_mod.F90').write_text(fcode_mod) + + scheduler = Scheduler(paths=[workdir], config=config, seed_routines=['driver'], frontend=frontend) + + class LoggingTransformation(Transformation): + + def __init__(self): + self.record = [] + + def transform_file(self, sourcefile, **kwargs): + self.record += [kwargs['item'].name + '::' + sourcefile.path.name] + + def transform_module(self, module, **kwargs): + self.record += [kwargs['item'].name + '::' + module.name] + + def transform_subroutine(self, routine, **kwargs): + self.record += [kwargs['item'].name + '::' + routine.name] + + transformation = LoggingTransformation() + scheduler.process( + transformation=transformation, reverse=reverse, use_file_graph=use_file_graph, + recurse_to_contained_nodes=recurse_to_contained_nodes + ) + + if use_file_graph: + if recurse_to_contained_nodes: + expected = [ + 'member_mod#driver::member_mod.F90', + 'member_mod#driver::member_mod', + 'member_mod#driver::my_routine', + 'member_mod#driver::driver', + 'member_mod#driver::my_member', + ] + else: + expected = ['member_mod#driver::member_mod.F90'] else: - assert transformation.record == expected + if recurse_to_contained_nodes: + expected = [ + [ + 'member_mod#driver::driver', + 'member_mod#driver::my_member', + ], [ + 'member_mod#driver#my_member::my_member', + ], [ + 'member_mod#my_routine::my_routine', + ] + ] + else: + expected = [ + 'member_mod#driver::driver', + 'member_mod#driver#my_member::my_member', + 'member_mod#my_routine::my_routine', + ] + + if not use_file_graph: + # Because the scheduler cannot represent contained member routines currently, it does + # not find the call dependencies via the member routine and therefore doesn't process + # these subroutines with the transformation + if len(scheduler.items) == 1 and transformation.record == flatten(expected[:1]): + pytest.xfail(reason='Scheduler unable to represent contained member routines as graph items') + + assert transformation.record == flatten(expected) + + rmtree(workdir) + @pytest.mark.parametrize('frontend', available_frontends()) From 7d02d80a7b4494dd2bddfb9eb1fd4eb0903487b4 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Wed, 6 Sep 2023 23:15:12 +0100 Subject: [PATCH 2/6] DependencyTrafo: pick out roles, targets from item and select correct item if called via recursion --- loki/transform/dependency_transform.py | 41 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/loki/transform/dependency_transform.py b/loki/transform/dependency_transform.py index e98c2c581..0d7f79901 100644 --- a/loki/transform/dependency_transform.py +++ b/loki/transform/dependency_transform.py @@ -79,6 +79,29 @@ def transform_subroutine(self, routine, **kwargs): """ role = kwargs.get('role') + # If applied recursively over all routines in a sourcefile, we may + # visit subroutines that are not part of the scheduler graph, such as + # contained member functions (for now). While it would be safe to rename + # these as well, we do not currently list them in the `targets` list and + # therefore avoid processing the routine to retain legacy behaviour + if 'items' in kwargs: + item_names = [item_.local_name for item_ in kwargs['items']] + if routine.name.lower() not in item_names: + return + + kwargs['item'] = kwargs['items'][item_names.index(routine.name.lower())] + + # If called without explicit role or target, extract from Item + # We need to do this here to cache the value for targets, because + # rename_calls will change this property + if (item := kwargs.get('item')): + if not role: + kwargs['role'] = item.role + role = item.role + if not kwargs.get('targets'): + kwargs['targets'] = item.targets + + if role == 'kernel': if routine.name.endswith(self.suffix): # This is to ensure that the transformation is idempotent if @@ -125,6 +148,8 @@ def transform_module(self, module, **kwargs): Rename kernel modules and re-point module-level imports. """ role = kwargs.get('role') + if not role and 'item' in kwargs: + role = kwargs['item'].role if role == 'kernel': # Change the name of kernel modules @@ -139,6 +164,8 @@ def transform_file(self, sourcefile, **kwargs): In 'module' mode perform module-wrapping for dependnecy injection. """ role = kwargs.get('role') + if not role and 'item' in kwargs: + role = kwargs['item'].role if role == 'kernel' and self.mode == 'module': self.module_wrap(sourcefile, **kwargs) @@ -149,7 +176,7 @@ def rename_calls(self, routine, **kwargs): :param targets: Optional list of subroutine names for which to modify the corresponding calls. """ - targets = as_tuple(kwargs.get('targets', None)) + targets = as_tuple(kwargs.get('targets')) targets = as_tuple(str(t).upper() for t in targets) members = [r.name for r in routine.subroutines] @@ -174,7 +201,9 @@ def rename_imports(self, source, imports, **kwargs): :param targets: Optional list of subroutine names for which to modify the corresponding calls. """ - targets = as_tuple(kwargs.get('targets', None)) + targets = as_tuple(kwargs.get('targets')) + if not targets and 'item' in kwargs: + targets = as_tuple(kwargs['item'].targets) targets = as_tuple(str(t).upper() for t in targets) # We don't want to rename module variable imports, so we build @@ -239,10 +268,12 @@ def rename_interfaces(self, source, intfs, **kwargs): Update explicit interfaces to actively transformed subroutines. """ - targets = as_tuple(kwargs.get('targets', None)) + targets = as_tuple(kwargs.get('targets')) + if not targets and 'item' in kwargs: + targets = as_tuple(kwargs['item'].targets) targets = as_tuple(str(t).lower() for t in targets) - if self.replace_ignore_items and (item := kwargs.get('item', None)): + if self.replace_ignore_items and (item := kwargs.get('item')): targets += as_tuple(str(i).lower() for i in item.ignore) # Transformer map to remove any outdated interfaces @@ -301,6 +332,8 @@ def module_wrap(self, sourcefile, **kwargs): Wrap target subroutines in modules and replace in source file. """ targets = as_tuple(kwargs.get('targets', None)) + if not targets and 'item' in kwargs: + targets = as_tuple(kwargs['item'].targets) targets = as_tuple(str(t).upper() for t in targets) item = kwargs.get('item', None) From 03a6f978b0debd0fe2f1f1bbcd0e5dc02d281c9e Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Wed, 6 Sep 2023 23:15:36 +0100 Subject: [PATCH 3/6] Add item filter to file graph scheduler traversal --- loki/bulk/scheduler.py | 4 ++++ scripts/loki_transform.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/loki/bulk/scheduler.py b/loki/bulk/scheduler.py index e7d8d93ac..0e520a73d 100644 --- a/loki/bulk/scheduler.py +++ b/loki/bulk/scheduler.py @@ -636,6 +636,10 @@ def process(self, transformation, reverse=False, item_filter=SubroutineItem, use if use_file_graph: for node in traversal: items = graph.nodes[node]['items'] + + if item_filter and any(not isinstance(item, item_filter) for item in items): + continue + transformation.apply( items[0].source, item=items[0], items=items, recurse_to_contained_nodes=recurse_to_contained_nodes diff --git a/scripts/loki_transform.py b/scripts/loki_transform.py index 92f9158f7..4c8fade01 100644 --- a/scripts/loki_transform.py +++ b/scripts/loki_transform.py @@ -303,8 +303,14 @@ def transform_subroutine(self, routine, **kwargs): scheduler.process(transformation=dependency, use_file_graph=True, recurse_to_contained_nodes=True) # Write out all modified source files into the build directory - scheduler.process(transformation=FileWriteTransformation(builddir=out_path, mode=mode, cuf='cuf' in mode), - use_file_graph=True) + if global_var_offload: + item_filter = (SubroutineItem, GlobalVarImportItem) + else: + item_filter = SubroutineItem + scheduler.process( + transformation=FileWriteTransformation(builddir=out_path, mode=mode, cuf='cuf' in mode), + use_file_graph=True, item_filter=item_filter + ) @cli.command() From 12d99c071c101c7fc95eccd488a7f788a460feb8 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Thu, 7 Sep 2023 10:44:41 +0100 Subject: [PATCH 4/6] Move DependencyTransformation tests to new file --- tests/test_transform_dependency.py | 452 ++++++++++++++++++++++++++ tests/test_transformation.py | 505 +---------------------------- 2 files changed, 455 insertions(+), 502 deletions(-) create mode 100644 tests/test_transform_dependency.py diff --git a/tests/test_transform_dependency.py b/tests/test_transform_dependency.py new file mode 100644 index 000000000..87c2c8e15 --- /dev/null +++ b/tests/test_transform_dependency.py @@ -0,0 +1,452 @@ +from pathlib import Path +import pytest + +from conftest import available_frontends +from loki import ( + OMNI, OFP, Sourcefile, CallStatement, Import, + FindNodes, FindInlineCalls, Intrinsic +) +from loki.transform import DependencyTransformation + + +@pytest.fixture(scope='module', name='here') +def fixture_here(): + return Path(__file__).parent + + +@pytest.mark.parametrize('frontend', available_frontends()) +def test_dependency_transformation_globalvar_imports(frontend): + """ + Test that global variable imports are not renamed as a + call statement would be. + """ + + kernel = Sourcefile.from_source(source=""" +MODULE kernel_mod + INTEGER :: some_const +CONTAINS + SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + a = 1 + b = 2 + c = 3 + END SUBROUTINE kernel +END MODULE kernel_mod +""", frontend=frontend) + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + USE kernel_mod, only: kernel + USE kernel_mod, only: some_const + INTEGER, INTENT(INOUT) :: a, b, c + + CALL kernel(a, b ,c) +END SUBROUTINE driver +""", frontend=frontend) + + transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') + # Because the renaming is intended to be applied to the routines as well as the enclosing module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets=('kernel', 'some_const')) + + # Check that the global variable declaration remains unchanged + assert kernel.modules[0].variables[0].name == 'some_const' + + # Check that calls and matching import have been diverted to the re-generated routine + calls = FindNodes(CallStatement).visit(driver['driver'].body) + assert len(calls) == 1 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 2 + assert isinstance(imports[0], Import) + assert driver['driver'].spec.body[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in driver['driver'].spec.body[0].symbols] + + # Check that global variable import remains unchanged + assert isinstance(imports[1], Import) + assert driver['driver'].spec.body[1].module == 'kernel_mod' + assert 'some_const' in [str(s) for s in driver['driver'].spec.body[1].symbols] + + +@pytest.mark.parametrize('frontend', available_frontends()) +def test_dependency_transformation_globalvar_imports_driver_mod(frontend): + """ + Test that global variable imports are not renamed as a + call statement would be. + """ + + kernel = Sourcefile.from_source(source=""" +MODULE kernel_mod + INTEGER :: some_const +CONTAINS + SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + a = 1 + b = 2 + c = 3 + END SUBROUTINE kernel +END MODULE kernel_mod +""", frontend=frontend) + + driver = Sourcefile.from_source(source=""" +MODULE DRIVER_MOD + USE kernel_mod, only: kernel + USE kernel_mod, only: some_const +CONTAINS +SUBROUTINE driver(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + CALL kernel(a, b ,c) +END SUBROUTINE driver +END MODULE DRIVER_MOD +""", frontend=frontend) + + transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') + # Because the renaming is intended to be applied to the routines as well as the enclosing module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver.apply(transformation, role='driver', targets=('kernel', 'some_const'), recurse_to_contained_nodes=True) + + # Check that the global variable declaration remains unchanged + assert kernel.modules[0].variables[0].name == 'some_const' + + # Check that calls and matching import have been diverted to the re-generated routine + calls = FindNodes(CallStatement).visit(driver['driver'].body) + assert len(calls) == 1 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver_mod'].spec) + assert len(imports) == 2 + assert isinstance(imports[0], Import) + assert driver['driver_mod'].spec.body[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in driver['driver_mod'].spec.body[0].symbols] + + # Check that global variable import remains unchanged + assert isinstance(imports[1], Import) + assert driver['driver_mod'].spec.body[1].module == 'kernel_mod' + assert 'some_const' in [str(s) for s in driver['driver_mod'].spec.body[1].symbols] + + +@pytest.mark.parametrize('frontend', available_frontends(xfail=[(OMNI, 'C-imports need pre-processing for OMNI')])) +def test_dependency_transformation_header_includes(here, frontend): + """ + Test injection of suffixed kernels into unchanged driver + routines via c-header includes. + """ + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + +#include "kernel.intfb.h" + + CALL kernel(a, b ,c) +END SUBROUTINE driver +""", frontend=frontend) + + kernel = Sourcefile.from_source(source=""" +SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + a = 1 + b = 2 + c = 3 +END SUBROUTINE kernel +""", frontend=frontend) + + # Ensure header file does not exist a-priori + header_file = here/'kernel_test.intfb.h' + if header_file.exists(): + header_file.unlink() + + # Apply injection transformation via C-style includes by giving `include_path` + transformation = DependencyTransformation(suffix='_test', mode='strict', include_path=here) + kernel['kernel'].apply(transformation, role='kernel') + driver['driver'].apply(transformation, role='driver', targets='kernel') + + # Check that the subroutine name in the kernel source has changed + assert len(kernel.modules) == 0 + assert len(kernel.subroutines) == 1 + assert kernel.subroutines[0].name == 'kernel_test' + assert kernel['kernel_test'] == kernel.all_subroutines[0] + + # Check that the driver name has not changed + assert len(kernel.modules) == 0 + assert len(kernel.subroutines) == 1 + assert driver.subroutines[0].name == 'driver' + + # Check that the import has been updated + assert '#include "kernel.intfb.h"' not in driver.to_fortran() + assert '#include "kernel_test.intfb.h"' in driver.to_fortran() + + # Check that header file was generated and clean up + assert header_file.exists() + header_file.unlink() + + +@pytest.mark.parametrize('frontend', available_frontends(xfail=[(OMNI, 'C-imports need pre-processing for OMNI')])) +def test_dependency_transformation_module_wrap(frontend): + """ + Test injection of suffixed kernels into unchanged driver + routines automatic module wrapping of the kernel. + """ + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + +#include "kernel.intfb.h" + + CALL kernel(a, b ,c) +END SUBROUTINE driver +""", frontend=frontend) + + kernel = Sourcefile.from_source(source=""" +SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + a = 1 + b = 2 + c = 3 +END SUBROUTINE kernel +""", frontend=frontend) + + # Apply injection transformation via C-style includes by giving `include_path` + transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') + + # Check that the kernel has been wrapped + assert len(kernel.subroutines) == 0 + assert len(kernel.all_subroutines) == 1 + assert kernel.all_subroutines[0].name == 'kernel_test' + assert kernel['kernel_test'] == kernel.all_subroutines[0] + assert len(kernel.modules) == 1 + assert kernel.modules[0].name == 'kernel_test_mod' + assert kernel['kernel_test_mod'] == kernel.modules[0] + + # Check that the driver name has not changed + assert len(driver.modules) == 0 + assert len(driver.subroutines) == 1 + assert driver.subroutines[0].name == 'driver' + + # Check that calls and imports have been diverted to the re-generated routine + calls = FindNodes(CallStatement).visit(driver['driver'].body) + assert len(calls) == 1 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 1 + assert imports[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in imports[0].symbols] + + +@pytest.mark.parametrize('frontend', available_frontends()) +def test_dependency_transformation_replace_interface(frontend): + """ + Test injection of suffixed kernels defined in interface block + into unchanged driver routines automatic module wrapping of the kernel. + """ + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + IMPLICIT NONE + INTERFACE + SUBROUTINE KERNEL(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + END SUBROUTINE KERNEL + END INTERFACE + + INTEGER, INTENT(INOUT) :: a, b, c + + CALL kernel(a, b ,c) +END SUBROUTINE driver +""", frontend=frontend) + + kernel = Sourcefile.from_source(source=""" +SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + a = 1 + b = 2 + c = 3 +END SUBROUTINE kernel +""", frontend=frontend) + + # Apply injection transformation via C-style includes by giving `include_path` + transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') + + # Check that the kernel has been wrapped + assert len(kernel.subroutines) == 0 + assert len(kernel.all_subroutines) == 1 + assert kernel.all_subroutines[0].name == 'kernel_test' + assert kernel['kernel_test'] == kernel.all_subroutines[0] + assert len(kernel.modules) == 1 + assert kernel.modules[0].name == 'kernel_test_mod' + assert kernel['kernel_test_mod'] == kernel.modules[0] + + # Check that the driver name has not changed + assert len(driver.modules) == 0 + assert len(driver.subroutines) == 1 + assert driver.subroutines[0].name == 'driver' + + # Check that calls and imports have been diverted to the re-generated routine + calls = FindNodes(CallStatement).visit(driver['driver'].body) + assert len(calls) == 1 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 1 + if frontend == OMNI: + assert imports[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in imports[0].symbols] + else: + assert imports[0].module == 'KERNEL_test_mod' + assert 'KERNEL_test' in [str(s) for s in imports[0].symbols] + + # Check that the newly generated USE statement appears before IMPLICIT NONE + nodes = FindNodes((Intrinsic, Import)).visit(driver['driver'].spec) + assert len(nodes) == 2 + assert isinstance(nodes[1], Intrinsic) + assert nodes[1].text.lower() == 'implicit none' + + +@pytest.mark.parametrize('frontend', available_frontends( + xfail=[(OFP, 'OFP does not correctly handle result variable declaration.')])) +def test_dependency_transformation_inline_call(frontend): + """ + Test injection of suffixed kernel, accessed through inline function call. + """ + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + INTERFACE + INTEGER FUNCTION kernel(a) + INTEGER, INTENT(IN) :: a + END FUNCTION kernel + END INTERFACE + + INTEGER, INTENT(INOUT) :: a, b, c + + a = kernel(a) + b = kernel(a) + c = kernel(c) +END SUBROUTINE driver +""", frontend=frontend) + + kernel = Sourcefile.from_source(source=""" +INTEGER FUNCTION kernel(a) + INTEGER, INTENT(IN) :: a + + kernel = 2*a +END FUNCTION kernel +""", frontend=frontend) + + # Apply injection transformation via C-style includes by giving `include_path` + transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') + + # Check that the kernel has been wrapped + assert len(kernel.subroutines) == 0 + assert len(kernel.all_subroutines) == 1 + assert kernel.all_subroutines[0].name == 'kernel_test' + assert kernel['kernel_test'] == kernel.all_subroutines[0] + assert kernel['kernel_test'].is_function + assert len(kernel.modules) == 1 + assert kernel.modules[0].name == 'kernel_test_mod' + assert kernel['kernel_test_mod'] == kernel.modules[0] + + # Check that the return name has been added as a variable + assert 'kernel_test' in kernel['kernel_test'].variables + + # Check that the driver name has not changed + assert len(driver.modules) == 0 + assert len(driver.subroutines) == 1 + assert driver.subroutines[0].name == 'driver' + + # Check that calls and imports have been diverted to the re-generated routine + calls = tuple(FindInlineCalls().visit(driver['driver'].body)) + assert len(calls) == 2 + calls = tuple(FindInlineCalls(unique=False).visit(driver['driver'].body)) + assert len(calls) == 3 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 1 + assert imports[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in imports[0].symbols] + + +@pytest.mark.parametrize('frontend', available_frontends( + xfail=[(OFP, 'OFP does not correctly handle result variable declaration.')])) +def test_dependency_transformation_inline_call_result_var(frontend): + """ + Test injection of suffixed kernel, accessed through inline function call. + """ + + driver = Sourcefile.from_source(source=""" +SUBROUTINE driver(a, b, c) + INTERFACE + FUNCTION kernel(a) RESULT(ret) + INTEGER, INTENT(IN) :: a + INTEGER :: ret + END FUNCTION kernel + END INTERFACE + + INTEGER, INTENT(INOUT) :: a, b, c + + a = kernel(a) + b = kernel(a) + c = kernel(c) +END SUBROUTINE driver +""", frontend=frontend) + + kernel = Sourcefile.from_source(source=""" +FUNCTION kernel(a) RESULT(ret) + INTEGER, INTENT(IN) :: a + INTEGER :: ret + + ret = 2*a +END FUNCTION kernel +""", frontend=frontend) + + # Apply injection transformation via C-style includes by giving `include_path` + transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') + + # Check that the kernel has been wrapped + assert len(kernel.subroutines) == 0 + assert len(kernel.all_subroutines) == 1 + assert kernel.all_subroutines[0].name == 'kernel_test' + assert kernel['kernel_test'] == kernel.all_subroutines[0] + assert kernel['kernel_test'].is_function + assert len(kernel.modules) == 1 + assert kernel.modules[0].name == 'kernel_test_mod' + assert kernel['kernel_test_mod'] == kernel.modules[0] + + # Check that the driver name has not changed + assert len(driver.modules) == 0 + assert len(driver.subroutines) == 1 + assert driver.subroutines[0].name == 'driver' + + # Check that calls and imports have been diverted to the re-generated routine + calls = tuple(FindInlineCalls().visit(driver['driver'].body)) + assert len(calls) == 2 + calls = tuple(FindInlineCalls(unique=False).visit(driver['driver'].body)) + assert len(calls) == 3 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 1 + assert imports[0].module == 'kernel_test_mod' + assert 'kernel_test' in [str(s) for s in imports[0].symbols] diff --git a/tests/test_transformation.py b/tests/test_transformation.py index bea948aaf..d06f69154 100644 --- a/tests/test_transformation.py +++ b/tests/test_transformation.py @@ -3,13 +3,12 @@ from conftest import jit_compile, clean_test, available_frontends from loki import ( - OMNI, REGEX, OFP, Sourcefile, Subroutine, CallStatement, Import, + OMNI, REGEX, Sourcefile, Subroutine, Import, FindNodes, FindInlineCalls, fgen, Assignment, IntLiteral, Module, - SubroutineItem, Intrinsic, Comment + SubroutineItem, Comment ) from loki.transform import ( - Transformation, DependencyTransformation, replace_selected_kind, - FileWriteTransformation + Transformation, replace_selected_kind, FileWriteTransformation ) @@ -235,504 +234,6 @@ def test_transformation_apply_module(rename_transform, frontend, apply_method, l assert source['module_routine'] == source.all_subroutines[1] -@pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_module_imports(frontend): - """ - Test injection of suffixed kernels into unchanged driver - routines via module imports. - """ - - kernel = Sourcefile.from_source(source=""" -MODULE kernel_mod -CONTAINS - SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 - END SUBROUTINE kernel -END MODULE kernel_mod -""", frontend=frontend) - - driver = Sourcefile.from_source(source=""" -MODULE driver_mod - USE kernel_mod, only: kernel -CONTAINS - SUBROUTINE driver(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - CALL kernel(a, b ,c) - END SUBROUTINE driver -END MODULE driver_mod -""", frontend=frontend) - - transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') - # Because the renaming is intended to be applied to the routines as well as the enclosing module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver.apply(transformation, role='driver', targets='kernel', recurse_to_contained_nodes=True) - - # Check that the basic entity names in the kernel source have changed - assert kernel.all_subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - assert kernel.modules[0].name == 'kernel_test_mod' - assert kernel['kernel_test_mod'] == kernel.modules[0] - - # Check that the entity names in the driver have not changed - assert driver.all_subroutines[0].name == 'driver' - assert driver['driver'] == driver.all_subroutines[0] - assert driver.modules[0].name == 'driver_mod' - assert driver['driver_mod'] == driver.modules[0] - - # Check that calls and imports have been diverted to the re-generated routine - calls = FindNodes(CallStatement).visit(driver['driver'].body) - assert len(calls) == 1 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver_mod'].spec) - assert len(imports) == 1 - assert isinstance(imports[0], Import) - assert driver['driver_mod'].spec.body[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in driver['driver_mod'].spec.body[0].symbols] - - -@pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_globalvar_imports(frontend): - """ - Test that global variable imports are not renamed as a - call statement would be. - """ - - kernel = Sourcefile.from_source(source=""" -MODULE kernel_mod - INTEGER :: some_const -CONTAINS - SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 - END SUBROUTINE kernel -END MODULE kernel_mod -""", frontend=frontend) - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - USE kernel_mod, only: kernel - USE kernel_mod, only: some_const - INTEGER, INTENT(INOUT) :: a, b, c - - CALL kernel(a, b ,c) -END SUBROUTINE driver -""", frontend=frontend) - - transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') - # Because the renaming is intended to be applied to the routines as well as the enclosing module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets=('kernel', 'some_const')) - - # Check that the global variable declaration remains unchanged - assert kernel.modules[0].variables[0].name == 'some_const' - - # Check that calls and matching import have been diverted to the re-generated routine - calls = FindNodes(CallStatement).visit(driver['driver'].body) - assert len(calls) == 1 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver'].spec) - assert len(imports) == 2 - assert isinstance(imports[0], Import) - assert driver['driver'].spec.body[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in driver['driver'].spec.body[0].symbols] - - # Check that global variable import remains unchanged - assert isinstance(imports[1], Import) - assert driver['driver'].spec.body[1].module == 'kernel_mod' - assert 'some_const' in [str(s) for s in driver['driver'].spec.body[1].symbols] - - -@pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_globalvar_imports_driver_mod(frontend): - """ - Test that global variable imports are not renamed as a - call statement would be. - """ - - kernel = Sourcefile.from_source(source=""" -MODULE kernel_mod - INTEGER :: some_const -CONTAINS - SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 - END SUBROUTINE kernel -END MODULE kernel_mod -""", frontend=frontend) - - driver = Sourcefile.from_source(source=""" -MODULE DRIVER_MOD - USE kernel_mod, only: kernel - USE kernel_mod, only: some_const -CONTAINS -SUBROUTINE driver(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - CALL kernel(a, b ,c) -END SUBROUTINE driver -END MODULE DRIVER_MOD -""", frontend=frontend) - - transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') - # Because the renaming is intended to be applied to the routines as well as the enclosing module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver.apply(transformation, role='driver', targets=('kernel', 'some_const'), recurse_to_contained_nodes=True) - - # Check that the global variable declaration remains unchanged - assert kernel.modules[0].variables[0].name == 'some_const' - - # Check that calls and matching import have been diverted to the re-generated routine - calls = FindNodes(CallStatement).visit(driver['driver'].body) - assert len(calls) == 1 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver_mod'].spec) - assert len(imports) == 2 - assert isinstance(imports[0], Import) - assert driver['driver_mod'].spec.body[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in driver['driver_mod'].spec.body[0].symbols] - - # Check that global variable import remains unchanged - assert isinstance(imports[1], Import) - assert driver['driver_mod'].spec.body[1].module == 'kernel_mod' - assert 'some_const' in [str(s) for s in driver['driver_mod'].spec.body[1].symbols] - -@pytest.mark.parametrize('frontend', available_frontends(xfail=[(OMNI, 'C-imports need pre-processing for OMNI')])) -def test_dependency_transformation_header_includes(here, frontend): - """ - Test injection of suffixed kernels into unchanged driver - routines via c-header includes. - """ - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - -#include "kernel.intfb.h" - - CALL kernel(a, b ,c) -END SUBROUTINE driver -""", frontend=frontend) - - kernel = Sourcefile.from_source(source=""" -SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 -END SUBROUTINE kernel -""", frontend=frontend) - - # Ensure header file does not exist a-priori - header_file = here/'kernel_test.intfb.h' - if header_file.exists(): - header_file.unlink() - - # Apply injection transformation via C-style includes by giving `include_path` - transformation = DependencyTransformation(suffix='_test', mode='strict', include_path=here) - kernel['kernel'].apply(transformation, role='kernel') - driver['driver'].apply(transformation, role='driver', targets='kernel') - - # Check that the subroutine name in the kernel source has changed - assert len(kernel.modules) == 0 - assert len(kernel.subroutines) == 1 - assert kernel.subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - - # Check that the driver name has not changed - assert len(kernel.modules) == 0 - assert len(kernel.subroutines) == 1 - assert driver.subroutines[0].name == 'driver' - - # Check that the import has been updated - assert '#include "kernel.intfb.h"' not in driver.to_fortran() - assert '#include "kernel_test.intfb.h"' in driver.to_fortran() - - # Check that header file was generated and clean up - assert header_file.exists() - header_file.unlink() - - -@pytest.mark.parametrize('frontend', available_frontends(xfail=[(OMNI, 'C-imports need pre-processing for OMNI')])) -def test_dependency_transformation_module_wrap(frontend): - """ - Test injection of suffixed kernels into unchanged driver - routines automatic module wrapping of the kernel. - """ - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - -#include "kernel.intfb.h" - - CALL kernel(a, b ,c) -END SUBROUTINE driver -""", frontend=frontend) - - kernel = Sourcefile.from_source(source=""" -SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 -END SUBROUTINE kernel -""", frontend=frontend) - - # Apply injection transformation via C-style includes by giving `include_path` - transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') - - # Check that the kernel has been wrapped - assert len(kernel.subroutines) == 0 - assert len(kernel.all_subroutines) == 1 - assert kernel.all_subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - assert len(kernel.modules) == 1 - assert kernel.modules[0].name == 'kernel_test_mod' - assert kernel['kernel_test_mod'] == kernel.modules[0] - - # Check that the driver name has not changed - assert len(driver.modules) == 0 - assert len(driver.subroutines) == 1 - assert driver.subroutines[0].name == 'driver' - - # Check that calls and imports have been diverted to the re-generated routine - calls = FindNodes(CallStatement).visit(driver['driver'].body) - assert len(calls) == 1 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver'].spec) - assert len(imports) == 1 - assert imports[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in imports[0].symbols] - - -@pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_replace_interface(frontend): - """ - Test injection of suffixed kernels defined in interface block - into unchanged driver routines automatic module wrapping of the kernel. - """ - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - IMPLICIT NONE - INTERFACE - SUBROUTINE KERNEL(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - END SUBROUTINE KERNEL - END INTERFACE - - INTEGER, INTENT(INOUT) :: a, b, c - - CALL kernel(a, b ,c) -END SUBROUTINE driver -""", frontend=frontend) - - kernel = Sourcefile.from_source(source=""" -SUBROUTINE kernel(a, b, c) - INTEGER, INTENT(INOUT) :: a, b, c - - a = 1 - b = 2 - c = 3 -END SUBROUTINE kernel -""", frontend=frontend) - - # Apply injection transformation via C-style includes by giving `include_path` - transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') - - # Check that the kernel has been wrapped - assert len(kernel.subroutines) == 0 - assert len(kernel.all_subroutines) == 1 - assert kernel.all_subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - assert len(kernel.modules) == 1 - assert kernel.modules[0].name == 'kernel_test_mod' - assert kernel['kernel_test_mod'] == kernel.modules[0] - - # Check that the driver name has not changed - assert len(driver.modules) == 0 - assert len(driver.subroutines) == 1 - assert driver.subroutines[0].name == 'driver' - - # Check that calls and imports have been diverted to the re-generated routine - calls = FindNodes(CallStatement).visit(driver['driver'].body) - assert len(calls) == 1 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver'].spec) - assert len(imports) == 1 - if frontend == OMNI: - assert imports[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in imports[0].symbols] - else: - assert imports[0].module == 'KERNEL_test_mod' - assert 'KERNEL_test' in [str(s) for s in imports[0].symbols] - - # Check that the newly generated USE statement appears before IMPLICIT NONE - nodes = FindNodes((Intrinsic, Import)).visit(driver['driver'].spec) - assert len(nodes) == 2 - assert isinstance(nodes[1], Intrinsic) - assert nodes[1].text.lower() == 'implicit none' - - -@pytest.mark.parametrize('frontend', available_frontends( - xfail=[(OFP, 'OFP does not correctly handle result variable declaration.')])) -def test_dependency_transformation_inline_call(frontend): - """ - Test injection of suffixed kernel, accessed through inline function call. - """ - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - INTERFACE - INTEGER FUNCTION kernel(a) - INTEGER, INTENT(IN) :: a - END FUNCTION kernel - END INTERFACE - - INTEGER, INTENT(INOUT) :: a, b, c - - a = kernel(a) - b = kernel(a) - c = kernel(c) -END SUBROUTINE driver -""", frontend=frontend) - - kernel = Sourcefile.from_source(source=""" -INTEGER FUNCTION kernel(a) - INTEGER, INTENT(IN) :: a - - kernel = 2*a -END FUNCTION kernel -""", frontend=frontend) - - # Apply injection transformation via C-style includes by giving `include_path` - transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') - - # Check that the kernel has been wrapped - assert len(kernel.subroutines) == 0 - assert len(kernel.all_subroutines) == 1 - assert kernel.all_subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - assert kernel['kernel_test'].is_function - assert len(kernel.modules) == 1 - assert kernel.modules[0].name == 'kernel_test_mod' - assert kernel['kernel_test_mod'] == kernel.modules[0] - - # Check that the return name has been added as a variable - assert 'kernel_test' in kernel['kernel_test'].variables - - # Check that the driver name has not changed - assert len(driver.modules) == 0 - assert len(driver.subroutines) == 1 - assert driver.subroutines[0].name == 'driver' - - # Check that calls and imports have been diverted to the re-generated routine - calls = tuple(FindInlineCalls().visit(driver['driver'].body)) - assert len(calls) == 2 - calls = tuple(FindInlineCalls(unique=False).visit(driver['driver'].body)) - assert len(calls) == 3 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver'].spec) - assert len(imports) == 1 - assert imports[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in imports[0].symbols] - - -@pytest.mark.parametrize('frontend', available_frontends( - xfail=[(OFP, 'OFP does not correctly handle result variable declaration.')])) -def test_dependency_transformation_inline_call_result_var(frontend): - """ - Test injection of suffixed kernel, accessed through inline function call. - """ - - driver = Sourcefile.from_source(source=""" -SUBROUTINE driver(a, b, c) - INTERFACE - FUNCTION kernel(a) RESULT(ret) - INTEGER, INTENT(IN) :: a - INTEGER :: ret - END FUNCTION kernel - END INTERFACE - - INTEGER, INTENT(INOUT) :: a, b, c - - a = kernel(a) - b = kernel(a) - c = kernel(c) -END SUBROUTINE driver -""", frontend=frontend) - - kernel = Sourcefile.from_source(source=""" -FUNCTION kernel(a) RESULT(ret) - INTEGER, INTENT(IN) :: a - INTEGER :: ret - - ret = 2*a -END FUNCTION kernel -""", frontend=frontend) - - # Apply injection transformation via C-style includes by giving `include_path` - transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') - - # Check that the kernel has been wrapped - assert len(kernel.subroutines) == 0 - assert len(kernel.all_subroutines) == 1 - assert kernel.all_subroutines[0].name == 'kernel_test' - assert kernel['kernel_test'] == kernel.all_subroutines[0] - assert kernel['kernel_test'].is_function - assert len(kernel.modules) == 1 - assert kernel.modules[0].name == 'kernel_test_mod' - assert kernel['kernel_test_mod'] == kernel.modules[0] - - # Check that the driver name has not changed - assert len(driver.modules) == 0 - assert len(driver.subroutines) == 1 - assert driver.subroutines[0].name == 'driver' - - # Check that calls and imports have been diverted to the re-generated routine - calls = tuple(FindInlineCalls().visit(driver['driver'].body)) - assert len(calls) == 2 - calls = tuple(FindInlineCalls(unique=False).visit(driver['driver'].body)) - assert len(calls) == 3 - assert calls[0].name == 'kernel_test' - imports = FindNodes(Import).visit(driver['driver'].spec) - assert len(imports) == 1 - assert imports[0].module == 'kernel_test_mod' - assert 'kernel_test' in [str(s) for s in imports[0].symbols] - - @pytest.mark.parametrize('frontend', available_frontends()) def test_transform_replace_selected_kind(here, frontend): """ From 581ea228b5927a91c24ca6c1e9b1444e174875b7 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Thu, 7 Sep 2023 13:07:22 +0100 Subject: [PATCH 5/6] DependencyTrafo: Add scheduler-based and contained member testing --- tests/test_transform_dependency.py | 253 ++++++++++++++++++++++++----- 1 file changed, 215 insertions(+), 38 deletions(-) diff --git a/tests/test_transform_dependency.py b/tests/test_transform_dependency.py index 87c2c8e15..33067ad82 100644 --- a/tests/test_transform_dependency.py +++ b/tests/test_transform_dependency.py @@ -1,10 +1,11 @@ from pathlib import Path +from shutil import rmtree import pytest from conftest import available_frontends from loki import ( - OMNI, OFP, Sourcefile, CallStatement, Import, - FindNodes, FindInlineCalls, Intrinsic + gettempdir, OMNI, OFP, Sourcefile, CallStatement, Import, + FindNodes, FindInlineCalls, Intrinsic, Scheduler, SchedulerConfig ) from loki.transform import DependencyTransformation @@ -13,15 +14,40 @@ def fixture_here(): return Path(__file__).parent +@pytest.fixture(scope='function', name='tempdir') +def fixture_tempdir(request): + basedir = gettempdir()/request.function.__name__ + basedir.mkdir(exist_ok=True) + yield basedir + if basedir.exists(): + rmtree(basedir) + + +@pytest.fixture(scope='function', name='config') +def fixture_config(): + return { + 'default': { + 'mode': 'idem', + 'role': 'kernel', + 'expand': True, + 'strict': True + }, + 'routine': [{ + 'name': 'driver', + 'role': 'driver', + }] + } + @pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_globalvar_imports(frontend): +@pytest.mark.parametrize('use_scheduler', [False, True]) +def test_dependency_transformation_globalvar_imports(frontend, use_scheduler, tempdir, config): """ Test that global variable imports are not renamed as a call statement would be. """ - kernel = Sourcefile.from_source(source=""" + kernel_fcode = """ MODULE kernel_mod INTEGER :: some_const CONTAINS @@ -33,9 +59,9 @@ def test_dependency_transformation_globalvar_imports(frontend): c = 3 END SUBROUTINE kernel END MODULE kernel_mod -""", frontend=frontend) + """.strip() - driver = Sourcefile.from_source(source=""" + driver_fcode = """ SUBROUTINE driver(a, b, c) USE kernel_mod, only: kernel USE kernel_mod, only: some_const @@ -43,13 +69,27 @@ def test_dependency_transformation_globalvar_imports(frontend): CALL kernel(a, b ,c) END SUBROUTINE driver -""", frontend=frontend) + """.strip() transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') - # Because the renaming is intended to be applied to the routines as well as the enclosing module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets=('kernel', 'some_const')) + + if use_scheduler: + (tempdir/'kernel_mod.F90').write_text(kernel_fcode) + (tempdir/'driver.F90').write_text(driver_fcode) + scheduler = Scheduler(paths=[tempdir], config=SchedulerConfig.from_dict(config), frontend=frontend) + scheduler.process(transformation, use_file_graph=True, recurse_to_contained_nodes=True) + + kernel = scheduler['kernel_mod#kernel'].source + driver = scheduler['#driver'].source + + else: + kernel = Sourcefile.from_source(kernel_fcode, frontend=frontend) + driver = Sourcefile.from_source(driver_fcode, frontend=frontend) + + # Because the renaming is intended to be applied to the routines as well as the enclosing module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets=('kernel', 'some_const')) # Check that the global variable declaration remains unchanged assert kernel.modules[0].variables[0].name == 'some_const' @@ -71,13 +111,14 @@ def test_dependency_transformation_globalvar_imports(frontend): @pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_globalvar_imports_driver_mod(frontend): +@pytest.mark.parametrize('use_scheduler', [False, True]) +def test_dependency_transformation_globalvar_imports_driver_mod(frontend, use_scheduler, tempdir, config): """ Test that global variable imports are not renamed as a call statement would be. """ - kernel = Sourcefile.from_source(source=""" + kernel_fcode = """ MODULE kernel_mod INTEGER :: some_const CONTAINS @@ -89,9 +130,9 @@ def test_dependency_transformation_globalvar_imports_driver_mod(frontend): c = 3 END SUBROUTINE kernel END MODULE kernel_mod -""", frontend=frontend) + """.strip() - driver = Sourcefile.from_source(source=""" + driver_fcode = """ MODULE DRIVER_MOD USE kernel_mod, only: kernel USE kernel_mod, only: some_const @@ -102,13 +143,27 @@ def test_dependency_transformation_globalvar_imports_driver_mod(frontend): CALL kernel(a, b ,c) END SUBROUTINE driver END MODULE DRIVER_MOD -""", frontend=frontend) + """.strip() transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') - # Because the renaming is intended to be applied to the routines as well as the enclosing module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver.apply(transformation, role='driver', targets=('kernel', 'some_const'), recurse_to_contained_nodes=True) + + if use_scheduler: + (tempdir/'kernel_mod.F90').write_text(kernel_fcode) + (tempdir/'driver_mod.F90').write_text(driver_fcode) + scheduler = Scheduler(paths=[tempdir], config=SchedulerConfig.from_dict(config), frontend=frontend) + scheduler.process(transformation, use_file_graph=True, recurse_to_contained_nodes=True) + + kernel = scheduler['kernel_mod#kernel'].source + driver = scheduler['driver_mod#driver'].source + + else: + kernel = Sourcefile.from_source(kernel_fcode, frontend=frontend) + driver = Sourcefile.from_source(driver_fcode, frontend=frontend) + + # Because the renaming is intended to be applied to the routines as well as the enclosing module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver.apply(transformation, role='driver', targets=('kernel', 'some_const'), recurse_to_contained_nodes=True) # Check that the global variable declaration remains unchanged assert kernel.modules[0].variables[0].name == 'some_const' @@ -187,13 +242,14 @@ def test_dependency_transformation_header_includes(here, frontend): @pytest.mark.parametrize('frontend', available_frontends(xfail=[(OMNI, 'C-imports need pre-processing for OMNI')])) -def test_dependency_transformation_module_wrap(frontend): +@pytest.mark.parametrize('use_scheduler', [False, True]) +def test_dependency_transformation_module_wrap(frontend, use_scheduler, tempdir, config): """ Test injection of suffixed kernels into unchanged driver routines automatic module wrapping of the kernel. """ - driver = Sourcefile.from_source(source=""" + driver_fcode = """ SUBROUTINE driver(a, b, c) INTEGER, INTENT(INOUT) :: a, b, c @@ -201,9 +257,9 @@ def test_dependency_transformation_module_wrap(frontend): CALL kernel(a, b ,c) END SUBROUTINE driver -""", frontend=frontend) + """.strip() - kernel = Sourcefile.from_source(source=""" + kernel_fcode = """ SUBROUTINE kernel(a, b, c) INTEGER, INTENT(INOUT) :: a, b, c @@ -211,14 +267,28 @@ def test_dependency_transformation_module_wrap(frontend): b = 2 c = 3 END SUBROUTINE kernel -""", frontend=frontend) + """.strip() # Apply injection transformation via C-style includes by giving `include_path` transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') + + if use_scheduler: + (tempdir/'kernel.F90').write_text(kernel_fcode) + (tempdir/'driver.F90').write_text(driver_fcode) + scheduler = Scheduler(paths=[tempdir], config=SchedulerConfig.from_dict(config), frontend=frontend) + scheduler.process(transformation, use_file_graph=True, recurse_to_contained_nodes=True) + + kernel = scheduler['#kernel'].source + driver = scheduler['#driver'].source + + else: + kernel = Sourcefile.from_source(kernel_fcode, frontend=frontend) + driver = Sourcefile.from_source(driver_fcode, frontend=frontend) + + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') # Check that the kernel has been wrapped assert len(kernel.subroutines) == 0 @@ -245,13 +315,14 @@ def test_dependency_transformation_module_wrap(frontend): @pytest.mark.parametrize('frontend', available_frontends()) -def test_dependency_transformation_replace_interface(frontend): +@pytest.mark.parametrize('use_scheduler', [False, True]) +def test_dependency_transformation_replace_interface(frontend, use_scheduler, tempdir, config): """ Test injection of suffixed kernels defined in interface block into unchanged driver routines automatic module wrapping of the kernel. """ - driver = Sourcefile.from_source(source=""" + driver_fcode = """ SUBROUTINE driver(a, b, c) IMPLICIT NONE INTERFACE @@ -264,9 +335,9 @@ def test_dependency_transformation_replace_interface(frontend): CALL kernel(a, b ,c) END SUBROUTINE driver -""", frontend=frontend) + """.strip() - kernel = Sourcefile.from_source(source=""" + kernel_fcode = """ SUBROUTINE kernel(a, b, c) INTEGER, INTENT(INOUT) :: a, b, c @@ -274,14 +345,28 @@ def test_dependency_transformation_replace_interface(frontend): b = 2 c = 3 END SUBROUTINE kernel -""", frontend=frontend) + """.strip() # Apply injection transformation via C-style includes by giving `include_path` transformation = DependencyTransformation(suffix='_test', mode='module', module_suffix='_mod') - # Because the renaming is intended to also wrap the kernel in a module, - # we need to invoke the transformation on the full source file and activate recursion to contained nodes - kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) - driver['driver'].apply(transformation, role='driver', targets='kernel') + + if use_scheduler: + (tempdir/'kernel.F90').write_text(kernel_fcode) + (tempdir/'driver.F90').write_text(driver_fcode) + scheduler = Scheduler(paths=[tempdir], config=SchedulerConfig.from_dict(config), frontend=frontend) + scheduler.process(transformation, use_file_graph=True, recurse_to_contained_nodes=True) + + kernel = scheduler['#kernel'].source + driver = scheduler['#driver'].source + + else: + kernel = Sourcefile.from_source(kernel_fcode, frontend=frontend) + driver = Sourcefile.from_source(driver_fcode, frontend=frontend) + + # Because the renaming is intended to also wrap the kernel in a module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets='kernel') # Check that the kernel has been wrapped assert len(kernel.subroutines) == 0 @@ -450,3 +535,95 @@ def test_dependency_transformation_inline_call_result_var(frontend): assert len(imports) == 1 assert imports[0].module == 'kernel_test_mod' assert 'kernel_test' in [str(s) for s in imports[0].symbols] + + +@pytest.mark.parametrize('frontend', available_frontends()) +@pytest.mark.parametrize('use_scheduler', [False, True]) +def test_dependency_transformation_contained_member(frontend, use_scheduler, tempdir, config): + """ + The scheduler currently does not recognize or allow processing contained member routines as part + of the scheduler graph traversal. This test ensures that even with the transformation class functionality + to recurse into contained members enabled, the dependency injection is not applied. + """ + + kernel_fcode = """ +MODULE kernel_mod + IMPLICIT NONE +CONTAINS + SUBROUTINE kernel(a, b, c) + INTEGER, INTENT(INOUT) :: a, b, c + + call set_a(1) + b = get_b() + c = 3 + + CONTAINS + + SUBROUTINE SET_A(VAL) + INTEGER, INTENT(IN) :: VAL + A = VAL + END SUBROUTINE SET_A + + FUNCTION GET_B() + INTEGER GET_B + GET_B = 2 + END FUNCTION GET_B + END SUBROUTINE kernel +END MODULE kernel_mod + """.strip() + + driver_fcode = """ +SUBROUTINE driver(a, b, c) + USE kernel_mod, only: kernel + IMPLICIT NONE + INTEGER, INTENT(INOUT) :: a, b, c + + CALL kernel(a, b ,c) +END SUBROUTINE driver + """.strip() + + + transformation = DependencyTransformation(suffix='_test', module_suffix='_mod') + + if use_scheduler: + (tempdir/'kernel_mod.F90').write_text(kernel_fcode) + (tempdir/'driver.F90').write_text(driver_fcode) + scheduler = Scheduler(paths=[tempdir], config=SchedulerConfig.from_dict(config), frontend=frontend) + scheduler.process(transformation, use_file_graph=True, recurse_to_contained_nodes=True) + + kernel = scheduler['kernel_mod#kernel'].source + driver = scheduler['#driver'].source + else: + kernel = Sourcefile.from_source(kernel_fcode, frontend=frontend) + driver = Sourcefile.from_source(driver_fcode, frontend=frontend) + + # Because the renaming is intended to be applied to the routines as well as the enclosing module, + # we need to invoke the transformation on the full source file and activate recursion to contained nodes + kernel.apply(transformation, role='kernel', targets=('set_a', 'get_b'), recurse_to_contained_nodes=True) + driver['driver'].apply(transformation, role='driver', targets=('kernel', 'some_const')) + + # Check that calls and matching import have been diverted to the re-generated routine + calls = FindNodes(CallStatement).visit(driver['driver'].body) + assert len(calls) == 1 + assert calls[0].name == 'kernel_test' + imports = FindNodes(Import).visit(driver['driver'].spec) + assert len(imports) == 1 + assert imports[0].module.lower() == 'kernel_test_mod' + assert imports[0].symbols == ('kernel_test',) + + # Check that the kernel has been renamed + assert kernel.modules[0].name.lower() == 'kernel_test_mod' + assert kernel.modules[0].subroutines[0].name.lower() == 'kernel_test' + + # Check if contained member has been renamed + assert kernel['kernel_test'].subroutines[0].name.lower() == 'set_a' + assert kernel['kernel_test'].subroutines[1].name.lower() == 'get_b' + + # Check if kernel calls have been renamed + calls = FindNodes(CallStatement).visit(kernel['kernel_test'].body) + assert len(calls) == 1 + assert calls[0].name == 'set_a' + + calls = FindInlineCalls(unique=False).visit(kernel['kernel_test'].body) + assert len(calls) == 1 + assert calls[0].name == 'get_b' From d4c926cb39422e08f7f52436e63e6da0a3bbe295 Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Thu, 7 Sep 2023 13:07:41 +0100 Subject: [PATCH 6/6] DependencyTrafo: Do not process contained members --- loki/transform/dependency_transform.py | 30 ++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/loki/transform/dependency_transform.py b/loki/transform/dependency_transform.py index 0d7f79901..90001592e 100644 --- a/loki/transform/dependency_transform.py +++ b/loki/transform/dependency_transform.py @@ -80,16 +80,15 @@ def transform_subroutine(self, routine, **kwargs): role = kwargs.get('role') # If applied recursively over all routines in a sourcefile, we may - # visit subroutines that are not part of the scheduler graph, such as - # contained member functions (for now). While it would be safe to rename - # these as well, we do not currently list them in the `targets` list and - # therefore avoid processing the routine to retain legacy behaviour + # visit contained member subroutines. Since these are not outward facing + # it is not necessary to update their name. + is_member = isinstance(routine.parent, Subroutine) + + # Pick out correct item for current subroutine if processed via recursion if 'items' in kwargs: item_names = [item_.local_name for item_ in kwargs['items']] - if routine.name.lower() not in item_names: - return - - kwargs['item'] = kwargs['items'][item_names.index(routine.name.lower())] + if routine.name.lower() in item_names: + kwargs['item'] = kwargs['items'][item_names.index(routine.name.lower())] # If called without explicit role or target, extract from Item # We need to do this here to cache the value for targets, because @@ -101,17 +100,14 @@ def transform_subroutine(self, routine, **kwargs): if not kwargs.get('targets'): kwargs['targets'] = item.targets - - if role == 'kernel': + if role == 'kernel' and not is_member: if routine.name.endswith(self.suffix): # This is to ensure that the transformation is idempotent if # applied more than once to a routine return - # Change the name of kernel routines - if routine.is_function: - if not routine.result_name: - self.update_result_var(routine) + if routine.is_function and not routine.result_name: + self.update_result_var(routine) routine.name += self.suffix self.rename_calls(routine, **kwargs) @@ -124,7 +120,7 @@ def transform_subroutine(self, routine, **kwargs): intfs = FindNodes(Interface).visit(routine.spec) self.rename_interfaces(routine, intfs=intfs, **kwargs) - if role == 'kernel' and self.mode == 'strict': + if role == 'kernel' and self.mode == 'strict' and not is_member: # Re-generate C-style interface header self.generate_interfaces(routine) @@ -178,7 +174,7 @@ def rename_calls(self, routine, **kwargs): """ targets = as_tuple(kwargs.get('targets')) targets = as_tuple(str(t).upper() for t in targets) - members = [r.name for r in routine.subroutines] + members = [r.name.upper() for r in routine.subroutines] if self.replace_ignore_items: item = kwargs.get('item', None) @@ -191,6 +187,8 @@ def rename_calls(self, routine, **kwargs): call._update(name=call.name.clone(name=f'{call.name}{self.suffix}')) for call in FindInlineCalls(unique=False).visit(routine.body): + if call.name.upper() in members: + continue if targets is None or call.name.upper() in targets: call.function = call.function.clone(name=f'{call.name}{self.suffix}')