diff --git a/cmake/loki_transform.cmake b/cmake/loki_transform.cmake index 1df27fcc1..1a09fe1ce 100644 --- a/cmake/loki_transform.cmake +++ b/cmake/loki_transform.cmake @@ -42,7 +42,7 @@ include( loki_transform_helpers ) function( loki_transform ) - set( options CPP ) + set( options CPP MULTIMODE ) set( oneValueArgs COMMAND MODE FRONTEND CONFIG BUILDDIR ) set( multiValueArgs OUTPUT DEPENDS SOURCES HEADERS INCLUDES DEFINITIONS OMNI_INCLUDE XMOD ) @@ -73,6 +73,10 @@ function( loki_transform ) list( APPEND _ARGS --cpp ) endif() + if ( _PAR_MULTIMODE ) + list( APPEND _ARGS --multimode ) + endif() + # Ensure transformation script and environment is available _loki_transform_env_setup() @@ -115,7 +119,7 @@ endfunction() function( loki_transform_plan ) - set( options NO_SOURCEDIR CPP ) + set( options NO_SOURCEDIR CPP MULTIMODE ) set( oneValueArgs MODE FRONTEND CONFIG BUILDDIR SOURCEDIR CALLGRAPH PLAN ) set( multiValueArgs SOURCES HEADERS ) @@ -152,6 +156,10 @@ function( loki_transform_plan ) ecbuild_critical( "No PLAN file specified for loki_transform_plan()" ) endif() + if ( _PAR_MULTIMODE ) + list( APPEND _ARGS --multimode ) + endif() + _loki_transform_env_setup() # Create a source transformation plan to tell CMake which files will be affected @@ -300,7 +308,7 @@ endfunction() function( loki_transform_target ) - set( options NO_PLAN_SOURCEDIR COPY_UNMODIFIED CPP CPP_PLAN ) + set( options NO_PLAN_SOURCEDIR COPY_UNMODIFIED CPP CPP_PLAN MULTIMODE ) set( single_value_args COMMAND MODE FRONTEND CONFIG PLAN ) set( multi_value_args TARGET SOURCES HEADERS DEFINITIONS INCLUDES ) @@ -334,6 +342,9 @@ function( loki_transform_target ) if( _PAR_T_CPP_PLAN ) list( APPEND _PLAN_OPTIONS CPP ) endif() + if ( _PAR_T_MULTIMODE ) + list( APPEND _PLAN_OPTIONS MULTIMODE ) + endif() if( _PAR_T_NO_PLAN_SOURCEDIR ) list( APPEND _PLAN_OPTIONS NO_SOURCEDIR ) endif() @@ -367,6 +378,9 @@ function( loki_transform_target ) if( _PAR_T_CPP ) list( APPEND _TRANSFORM_OPTIONS CPP ) endif() + if ( _PAR_T_MULTIMODE ) + list( APPEND _TRANSFORM_OPTIONS MULTIMODE ) + endif() loki_transform( COMMAND ${_PAR_T_COMMAND} diff --git a/loki/batch/configure.py b/loki/batch/configure.py index 188a62a72..6e19314be 100644 --- a/loki/batch/configure.py +++ b/loki/batch/configure.py @@ -432,6 +432,13 @@ def mode(self): """ return self.config.get('mode', None) + @property + def inherited_mode(self): + """ + Transformation "inherited_mode" for multi-mode processing + """ + return self.config.get('inherited_mode', None) + @property def expand(self): """ diff --git a/loki/batch/item.py b/loki/batch/item.py index fb543b7ba..8613a27f2 100644 --- a/loki/batch/item.py +++ b/loki/batch/item.py @@ -564,6 +564,14 @@ def path(self): """ return self.source.path + @property + def orig_path(self): + """ + The original filepath of the associated source file + necessary for planning when duplicating/renaming items + """ + return self.source.orig_path + class FileItem(Item): """ diff --git a/loki/batch/item_factory.py b/loki/batch/item_factory.py index 970ce0afe..196f9ad84 100644 --- a/loki/batch/item_factory.py +++ b/loki/batch/item_factory.py @@ -290,6 +290,7 @@ def get_or_create_item_from_item(self, name, item, config=None): # Create a new FileItem for the new source new_source.path = item.path.with_name(f'{scope_name or local_name}{item.path.suffix}') + new_source.orig_path = item.path file_item = self.get_or_create_file_item_from_source(new_source, config=config) # Get the definition items for the FileItem and return the new item @@ -346,6 +347,13 @@ def get_or_create_file_item_from_path(self, path, config, frontend_args=None): self.item_cache[item_name] = file_item return file_item + def get_file_item_from_source(self, source): + # Check for file item with the same source object + for item in self.item_cache.values(): + if isinstance(item, FileItem) and item.source is source: + return item + return None + def get_or_create_file_item_from_source(self, source, config): """ Utility method to create a :any:`FileItem` corresponding to a given source object @@ -368,9 +376,9 @@ def get_or_create_file_item_from_source(self, source, config): The config object from which the item configuration will be derived """ # Check for file item with the same source object - for item in self.item_cache.values(): - if isinstance(item, FileItem) and item.source is source: - return item + item_ = self.get_file_item_from_source(source) + if item_ is not None: + return item_ if not source.path: raise RuntimeError('Cannot create FileItem from source: Sourcefile has no path') diff --git a/loki/batch/scheduler.py b/loki/batch/scheduler.py index b51328e22..9131dbe99 100644 --- a/loki/batch/scheduler.py +++ b/loki/batch/scheduler.py @@ -5,6 +5,7 @@ # granted to it by virtue of its status as an intergovernmental organisation # nor does it submit to any jurisdiction. +import sys from enum import Enum, auto from os.path import commonpath from pathlib import Path @@ -202,6 +203,38 @@ def __init__(self, paths, config=None, seed_routines=None, preprocess=False, # Attach interprocedural call-tree information self._enrich() + def propagate_and_separate_modes(self, proc_strategy=ProcessingStrategy.DEFAULT): + from loki.transformations.dependency import SeparateModesKernel # pylint: disable=import-outside-toplevel + self._propagate_modes() + self.process_transformation(SeparateModesKernel(), proc_strategy=proc_strategy) + self._propagate_modes_set() + modes = {item.mode for item in self.items} + return as_tuple(modes) + + + def _propagate_modes(self): + driver_items = [item for item in self.items if item.role == 'driver'] + for item in driver_items: + module_file_items = self.sgraph.get_corresponding_module_and_file_item(item, self.item_factory) + for _item in module_file_items: + if _item is not None: + _item.config['mode'] = item.mode + descendants = self.sgraph.descendants(item, self.item_factory, + include_module_items=True, include_file_items=True) + for descendant in descendants: + if descendant is not None: + descendant.config.setdefault('inherited_mode', set()).add(item.mode) + + def _propagate_modes_set(self): + driver_items = [item for item in self.items if item.role == 'driver'] + for item in driver_items: + descendants = self.sgraph.descendants(item, self.item_factory, + include_module_items=True, include_file_items=True) + for descendant in descendants: + if descendant is not None: + descendant.config['mode'] = item.mode + descendant.config['inherited_mode'] = set() + @Timer(logger=info, text='[Loki::Scheduler] Performed initial source scan in {:.2f}s') def _discover(self): """ @@ -429,7 +462,7 @@ def process(self, transformation, proc_strategy=ProcessingStrategy.DEFAULT): Parameters ---------- - transformation : :any:`Transformation` or :any:`Pipeline` + transformation : :any:`Transformation` or :any:`Pipeline` or dict of :any:`Pipeline`s The transformation or transformation pipeline to apply proc_strategy : :any:`ProcessingStrategy` The processing strategy to use when applying the given @@ -441,13 +474,24 @@ def process(self, transformation, proc_strategy=ProcessingStrategy.DEFAULT): elif isinstance(transformation, Pipeline): self.process_pipeline(pipeline=transformation, proc_strategy=proc_strategy) + elif isinstance(transformation, dict): + assert all(isinstance(trafo, Pipeline) for trafo in transformation.values()) + modes = self.propagate_and_separate_modes(proc_strategy=proc_strategy) + # check if all required pipelines are available + for mode in modes: + if mode not in transformation: + msg = f'[Loki] ERROR: Pipeline or transformation mode {mode} not found in config file.\n' + sys.exit(msg) + # apply those pipelines + for mode in modes: + self.process_pipeline(pipeline=self.config.pipelines[mode], proc_strategy=proc_strategy, mode=mode) else: error('[Loki::Scheduler] Batch processing requires Transformation or Pipeline object') raise RuntimeError(f'Could not batch process {transformation}') - def process_pipeline(self, pipeline, proc_strategy=ProcessingStrategy.DEFAULT): + def process_pipeline(self, pipeline, proc_strategy=ProcessingStrategy.DEFAULT, mode=None): """ - Process a given :any:`Pipeline` by applying its assocaited + Process a given :any:`Pipeline` by applying its associated transformations in turn. Parameters @@ -457,11 +501,14 @@ def process_pipeline(self, pipeline, proc_strategy=ProcessingStrategy.DEFAULT): proc_strategy : :any:`ProcessingStrategy` The processing strategy to use when applying the given :data:`pipeline` to the scheduler's graph. + mode : str, optional + Transformation mode, selecting which code transformations/pipeline on which graph to apply. + Default: `None`, thus mode agnostic. """ for transformation in pipeline.transformations: - self.process_transformation(transformation, proc_strategy=proc_strategy) + self.process_transformation(transformation, proc_strategy=proc_strategy, mode=mode) - def process_transformation(self, transformation, proc_strategy=ProcessingStrategy.DEFAULT): + def process_transformation(self, transformation, proc_strategy=ProcessingStrategy.DEFAULT, mode=None): """ Process all :attr:`items` in the scheduler's graph @@ -489,6 +536,9 @@ def process_transformation(self, transformation, proc_strategy=ProcessingStrateg proc_strategy : :any:`ProcessingStrategy` The processing strategy to use when applying the given :data:`transformation` to the scheduler's graph. + mode : str, optional + Transformation mode, selecting which code transformations on which graph to apply. + Default: `None`, thus mode agnostic. """ def _get_definition_items(_item, sgraph_items): # For backward-compatibility with the DependencyTransform and LinterTransformation @@ -527,7 +577,8 @@ def _get_definition_items(_item, sgraph_items): sgraph_items = sgraph.items traversal = SFilter( graph, reverse=transformation.reverse_traversal, - include_external=self.config.default.get('strict', True) + include_external=self.config.default.get('strict', True), + mode=mode ) else: graph = self.sgraph @@ -535,7 +586,8 @@ def _get_definition_items(_item, sgraph_items): traversal = SFilter( graph, item_filter=item_filter, reverse=transformation.reverse_traversal, exclude_ignored=not transformation.process_ignored_items, - include_external=self.config.default.get('strict', True) + include_external=self.config.default.get('strict', True), + mode=mode ) # Collect common transformation arguments diff --git a/loki/batch/sfilter.py b/loki/batch/sfilter.py index 0b5ff7877..b3418264b 100644 --- a/loki/batch/sfilter.py +++ b/loki/batch/sfilter.py @@ -7,7 +7,7 @@ import networkx as nx -from loki.batch.item import Item, ExternalItem +from loki.batch.item import Item, ExternalItem, TypeDefItem, InterfaceItem __all__ = ['SFilter'] @@ -38,9 +38,12 @@ class SFilter: Exclude :any:`Item` objects that have the ``is_ignored`` property include_external : bool, optional Do not skip :any:`ExternalItem` in the iterator + mode : str, optional + Only include items having corresponding mode. """ - def __init__(self, sgraph, item_filter=None, reverse=False, exclude_ignored=False, include_external=False): + def __init__(self, sgraph, item_filter=None, reverse=False, exclude_ignored=False, include_external=False, + mode=None): self.sgraph = sgraph self.reverse = reverse if item_filter: @@ -49,6 +52,7 @@ def __init__(self, sgraph, item_filter=None, reverse=False, exclude_ignored=Fals self.item_filter = Item self.exclude_ignored = exclude_ignored self.include_external = include_external + self.mode = mode def __iter__(self): if self.reverse: @@ -68,5 +72,10 @@ def __next__(self): node_cls = type(node) if issubclass(node_cls, self.item_filter) and not (self.exclude_ignored and node.is_ignored): # We found the next item matching the filter (and which is not ignored, if applicable) - break + if self.mode is None: + break + if isinstance(node, (ExternalItem, TypeDefItem, InterfaceItem)): + break + if node.mode == self.mode: + break return node diff --git a/loki/batch/sgraph.py b/loki/batch/sgraph.py index 7d2a48f05..7053a40da 100644 --- a/loki/batch/sgraph.py +++ b/loki/batch/sgraph.py @@ -491,3 +491,44 @@ def export_to_file(self, dotfile_path): graph.render(path, view=False) except gviz.ExecutableNotFound as e: warning(f'[Loki] Failed to render callgraph due to graphviz error:\n {e}') + + def descendants(self, item, item_factory, include_module_items=False, include_file_items=False): + """ + Get all descendants of a given :data:`item`. + + Parameters + ---------- + item : :any:`Item` + The item node in the dependency graph for which to determine the successors + item_factory : :any:`ItemFactory` + The item factory to use. + include_module_items : bool, optional + Whether to include module items. + include_file_items : bool, optional + Whether to include file items. + """ + item_descendants = list(nx.descendants(self._graph, item)) + module_items = [] + file_items = [] + if include_module_items: + module_items = [item_factory.item_cache.get(_item.scope_name) + for _item in item_descendants if not _item.is_ignored] + if include_file_items: + file_items = [item_factory.get_file_item_from_source(_item.source) + for _item in item_descendants if not _item.is_ignored] + return item_descendants + module_items + file_items + + def get_corresponding_module_and_file_item(self, item, item_factory): + """ + Get corresponding module and file item of a given :data:`item`. + + Parameters + ---------- + item : :any:`Item` + The item node in the dependency graph for which to determine the successors + item_factory : :any:`ItemFactory` + The item factory to use. + """ + _items = (item_factory.item_cache.get(item.scope_name),) + _items += (item_factory.get_file_item_from_source(item.source),) + return _items diff --git a/loki/batch/tests/test_scheduler.py b/loki/batch/tests/test_scheduler.py index 3ea291e4a..a375e0c1c 100644 --- a/loki/batch/tests/test_scheduler.py +++ b/loki/batch/tests/test_scheduler.py @@ -76,10 +76,10 @@ nodes as ir, FindNodes, FindInlineCalls, FindVariables ) from loki.transformations import ( - DependencyTransformation, ModuleWrapTransformation, FileWriteTransformation + DependencyTransformation, ModuleWrapTransformation, FileWriteTransformation, + CMakePlanTransformation ) - pytestmark = pytest.mark.skipif(not HAVE_FP, reason='Fparser not available') @@ -1324,9 +1324,9 @@ def test_scheduler_item_dependencies(testdir, tmp_path): } }) - proj_hoist = testdir/'sources/projHoist' + proj_multi_mode = testdir/'sources/projHoist' - scheduler = Scheduler(paths=proj_hoist, config=config, xmods=[tmp_path]) + scheduler = Scheduler(paths=proj_multi_mode, config=config, xmods=[tmp_path]) assert tuple( call.name for call in scheduler['transformation_module_hoist#driver'].dependencies @@ -3674,3 +3674,212 @@ def test_scheduler_module_interface_import(frontend, tmp_path): ('#test_scheduler', 'mod_a#outer_type'), ('mod_a#outer_type', 'mod_b#inner_type') ) + +@pytest.mark.parametrize('as_modules', [False, True]) +@pytest.mark.parametrize('reinit_scheduler', [True, False]) +@pytest.mark.parametrize('wrong_pipeline_name', [True, False]) +def test_scheduler_multi_modes(testdir, tmp_path, reinit_scheduler, as_modules, wrong_pipeline_name): + """ + Make sure children are correct and unique for items + """ + config = SchedulerConfig.from_dict({ + 'default': {'role': 'kernel', 'expand': True, 'strict': False, 'mode': 'm1', 'replicate': True}, + 'routines': { + 'driver_0': {'role': 'driver', 'mode': 'm1', 'replicate': False}, + 'driver_1': {'role': 'driver', 'mode': 'm1', 'replicate': False}, + 'driver_2': {'role': 'driver', 'mode': 'm2', 'replicate': False}, + 'driver_3': {'role': 'driver', 'mode': 'm3', 'replicate': False}, + 'driver_4': {'role': 'driver', 'mode': 'm3', 'replicate': False}, + 'nested_subroutine_3': {'ignore': ['test1', 'test2']} + }, + 'transformations': { + 'Idem1': {'classname': 'IdemTransformation', 'module': 'loki.transformations'}, + 'Idem2': {'classname': 'IdemTransformation', 'module': 'loki.transformations'}, + 'Idem3': {'classname': 'IdemTransformation', 'module': 'loki.transformations'} + }, + 'pipelines': { + f'{"m1x" if wrong_pipeline_name else "m1"}': {'transformations': {'Idem1'}}, + 'm2': {'transformations': {'Idem2'}}, + 'm3': {'transformations': {'Idem3'}} + }, + }) + + if as_modules: + proj_multi_mode = testdir/'sources/projMultiModeModules' + else: + proj_multi_mode = testdir/'sources/projMultiMode' + + builddir = tmp_path/'scheduler_multi_driver_modes_dir' + builddir.mkdir(exist_ok=True) + + scheduler = Scheduler(paths=proj_multi_mode, config=config, xmods=[tmp_path], + output_dir=builddir) + if wrong_pipeline_name: + # check failure since pipeline 'm1' can't be found + with pytest.raises(SystemExit): + scheduler.process(config.pipelines, proc_strategy=ProcessingStrategy.PLAN) + return + scheduler.process(config.pipelines, proc_strategy=ProcessingStrategy.PLAN) + + _expected_item_mode_dic = { + 'm1': { + 'nested_subroutine_3_lokim1_mod#nested_subroutine_3_lokim1', + 'driver_0_mod#driver_0', + 'nested_subroutine_1_lokim1_mod#nested_subroutine_1_lokim1', + 'subroutine_3_lokim1_mod#subroutine_3_lokim1', + 'driver_1_mod#driver_1', + 'subroutine_1_mod#subroutine_1' + }, + 'm2': { + 'subroutine_2_mod#subroutine_2', + 'driver_2_mod#driver_2', + 'nested_subroutine_3_lokim2_mod#nested_subroutine_3_lokim2', + 'nested_subroutine_2_mod#nested_subroutine_2' + }, + 'm3': { + 'nested_subroutine_3_mod#nested_subroutine_3', + 'driver_3_mod#driver_3', + 'driver_4_mod#driver_4', + 'nested_subroutine_1_mod#nested_subroutine_1', + 'subroutine_3_mod#subroutine_3' + } + } + if as_modules: + expected_item_mode_dic = _expected_item_mode_dic + else: + expected_item_mode_dic = {k: {f"#{v.split('#')[-1]}" if 'routine' in v else v for v in vals} + for k, vals in _expected_item_mode_dic.items()} + + items = scheduler.items + item_mode_dic = {} + for item in items: + item_mode_dic.setdefault(item.mode, set()).add(item.name) + + assert set(item_mode_dic.keys()) == set(expected_item_mode_dic.keys()) + for _mode, _val in item_mode_dic.items(): + assert expected_item_mode_dic[_mode] == _val + + transformations = ( + ModuleWrapTransformation(module_suffix='_mod'), + DependencyTransformation(suffix='_test', module_suffix='_mod'), + FileWriteTransformation() + ) + for transformation in transformations: + scheduler.process(transformation, proc_strategy=ProcessingStrategy.PLAN) + + plan_trafo = CMakePlanTransformation(rootpath=proj_multi_mode) + scheduler.process( + transformation=plan_trafo, + proc_strategy=ProcessingStrategy.PLAN + ) + ## checking planfile + planfile = tmp_path/'planfile' + plan_trafo.write_plan(planfile) + + loki_plan = planfile.read_text() + + # Validate the plan file content + plan_pattern = re.compile(r'set\(\s*(\w+)\s*(.*?)\s*\)', re.DOTALL) + + # loki_plan = planfile.read_text() + plan_dict = {k: v.split() for k, v in plan_pattern.findall(loki_plan)} + plan_dict = {k: {Path(s).stem for s in v} for k, v in plan_dict.items()} + + expected_keys = {'LOKI_SOURCES_TO_TRANSFORM', 'LOKI_SOURCES_TO_APPEND', 'LOKI_SOURCES_TO_REMOVE'} + assert set(plan_dict.keys()) == expected_keys + + _expected_files_to_transform = { + 'driver_0_mod', 'driver_1_mod', 'driver_2_mod', 'driver_3_mod', 'driver_4_mod', + 'nested_subroutine_1_mod', 'nested_subroutine_2_mod', 'nested_subroutine_3_mod', + 'subroutine_1_mod', 'subroutine_2_mod', 'subroutine_3_mod' + } + if as_modules: + expected_files_to_transform = _expected_files_to_transform + else: + expected_files_to_transform = {v.replace('_mod', '') + if 'routine' in v else v for v in _expected_files_to_transform} + _expected_files_to_append = { + 'driver_0_mod.m1', 'driver_1_mod.m1', 'driver_2_mod.m2', 'driver_3_mod.m3', + 'driver_4_mod.m3', 'nested_subroutine_1_mod.m3', 'nested_subroutine_1_lokim1_mod.m1', + 'nested_subroutine_2_mod.m2', 'nested_subroutine_3_mod.m3', 'nested_subroutine_3_lokim1_mod.m1', + 'nested_subroutine_3_lokim2_mod.m2', 'subroutine_1_mod.m1', 'subroutine_2_mod.m2', + 'subroutine_3_mod.m3', 'subroutine_3_lokim1_mod.m1' + } + if as_modules: + expected_files_to_append = _expected_files_to_append + else: + expected_files_to_append = {v.replace('_mod', '') if 'routine' in v else v for v in _expected_files_to_append} + expected_files_to_remove = { + 'driver_0_mod', 'driver_1_mod', 'driver_2_mod', 'driver_3_mod', 'driver_4_mod' + } + + assert set(plan_dict['LOKI_SOURCES_TO_TRANSFORM']) == expected_files_to_transform + assert set(plan_dict['LOKI_SOURCES_TO_APPEND']) == expected_files_to_append + assert set(plan_dict['LOKI_SOURCES_TO_REMOVE']) == expected_files_to_remove + + if reinit_scheduler: + scheduler = Scheduler(paths=proj_multi_mode, config=config, xmods=[tmp_path], + output_dir=builddir) + scheduler.propagate_and_separate_modes() + for transformation in transformations: + scheduler.process(transformation) + + expected_callgraph = { + 'driver_0_mod#driver_0': { + 'subroutine_1_test_mod#subroutine_1_test' : { + 'nested_subroutine_1_lokim1_test_mod#nested_subroutine_1_lokim1_test', + }, + 'subroutine_3_lokim1_test_mod#subroutine_3_lokim1_test': { + 'nested_subroutine_1_lokim1_test_mod#nested_subroutine_1_lokim1_test', + 'nested_subroutine_3_lokim1_test_mod#nested_subroutine_3_lokim1_test', + }, + }, + 'driver_1_mod#driver_1': { + 'subroutine_1_test_mod#subroutine_1_test' : { + 'nested_subroutine_1_lokim1_test_mod#nested_subroutine_1_lokim1_test', + }, + 'subroutine_3_lokim1_test_mod#subroutine_3_lokim1_test': { + 'nested_subroutine_1_lokim1_test_mod#nested_subroutine_1_lokim1_test', + 'nested_subroutine_3_lokim1_test_mod#nested_subroutine_3_lokim1_test', + } + }, + 'driver_2_mod#driver_2': { + 'subroutine_2_test_mod#subroutine_2_test' : { + 'nested_subroutine_2_test_mod#nested_subroutine_2_test', + 'nested_subroutine_3_lokim2_test_mod#nested_subroutine_3_lokim2_test' + } + }, + 'driver_3_mod#driver_3': { + 'subroutine_3_test_mod#subroutine_3_test': { + 'nested_subroutine_1_test_mod#nested_subroutine_1_test', + 'nested_subroutine_3_test_mod#nested_subroutine_3_test', + } + }, + 'driver_4_mod#driver_4': { + 'subroutine_3_test_mod#subroutine_3_test': { + 'nested_subroutine_1_test_mod#nested_subroutine_1_test', + 'nested_subroutine_3_test_mod#nested_subroutine_3_test', + } + } + } + + def check_callgraph(callgraph): + if not isinstance(callgraph, dict) or not callgraph: + return + for routine in callgraph.keys(): + routine_ir = scheduler[routine].ir + imports = routine_ir.imports + imported_symbols = () + for imp in imports: + imported_symbols += imp.symbols + successors = [] + for successor in callgraph[routine]: + successors.append(scheduler[successor].ir) + successors_local_name = [str(successor.name).lower() for successor in successors] + calls = FindNodes(ir.CallStatement).visit(routine_ir.body) + for call in calls: + assert str(call.name).lower() in successors_local_name + assert str(call.name).lower() in imported_symbols + check_callgraph(callgraph[routine]) + + check_callgraph(expected_callgraph) diff --git a/loki/cli/loki_transform.py b/loki/cli/loki_transform.py index 0d0320959..5d4099fd9 100644 --- a/loki/cli/loki_transform.py +++ b/loki/cli/loki_transform.py @@ -39,8 +39,10 @@ @click.option('--log-level', '-l', default='info', envvar='LOKI_LOGGING', type=click.Choice(['debug', 'detail', 'perf', 'info', 'warning', 'error']), help='Log level to output during batch processing') +@click.option('--multimode', '-z', is_flag=True, help="Multi-mode processing.") def convert( - frontend_opts, scheduler_opts, mode, config, plan_file, callgraph, root, log_level + frontend_opts, scheduler_opts, mode, config, plan_file, callgraph, root, log_level, + multimode ): """ Batch-processing mode for Fortran-to-Fortran transformations that @@ -88,18 +90,23 @@ def convert( definitions=definitions, output_dir=scheduler_opts.build, **frontend_opts.asdict ) - # If requested, apply a custom pipeline from the scheduler config - # Note that this new entry point will bypass all other default - # behaviour and exit immediately after. - if mode not in config.pipelines: - msg = f'[Loki] ERROR: Pipeline or transformation mode {mode} not found in config file.\n' - msg += '[Loki] Please provide a config file with configured transformation or pipelines instead.\n' - sys.exit(msg) + if multimode: + # multimode, therefore pass all available pipelines and let process handle the rest + info('[Loki-transform] Applying custom pipelines from config') + scheduler.process(config.pipelines, proc_strategy=processing_strategy) + else: + # If requested, apply a custom pipeline from the scheduler config + # Note that this new entry point will bypass all other default + # behaviour and exit immediately after. + if mode not in config.pipelines: + msg = f'[Loki] ERROR: Pipeline or transformation mode {mode} not found in config file.\n' + msg += '[Loki] Please provide a config file with configured transformation or pipelines instead.\n' + sys.exit(msg) - info(f'[Loki-transform] Applying custom pipeline {mode} from config:') - info(str(config.pipelines[mode])) + info(f'[Loki-transform] Applying custom pipeline {mode} from config:') + info(str(config.pipelines[mode])) - scheduler.process(config.pipelines[mode], proc_strategy=processing_strategy) + scheduler.process(config.pipelines[mode], proc_strategy=processing_strategy) mode = mode.replace('-', '_') # Sanitize mode string @@ -132,6 +139,7 @@ def convert( @click.option('--log-level', '-l', default='info', envvar='LOKI_LOGGING', type=click.Choice(['debug', 'detail', 'perf', 'info', 'warning', 'error']), help='Log level to output during batch processing') +@click.option('--multimode', '-z', is_flag=True, help="Multi-mode processing.") @click.pass_context def plan(ctx, *_args, **_kwargs): """ diff --git a/loki/sourcefile.py b/loki/sourcefile.py index d6d1d7703..9ac4edcc8 100644 --- a/loki/sourcefile.py +++ b/loki/sourcefile.py @@ -61,6 +61,7 @@ class Sourcefile: def __init__(self, path, ir=None, ast=None, source=None, incomplete=False, parser_classes=None): self.path = Path(path) if path is not None else path + self.orig_path = self.path if ir is not None and not isinstance(ir, Section): ir = Section(body=ir) self.ir = ir diff --git a/loki/tests/sources/projMultiMode/driver_0_mod.F90 b/loki/tests/sources/projMultiMode/driver_0_mod.F90 new file mode 100644 index 000000000..5058135eb --- /dev/null +++ b/loki/tests/sources/projMultiMode/driver_0_mod.F90 @@ -0,0 +1,12 @@ +module driver_0_mod + implicit none +contains + + subroutine driver_0() + #include "subroutine_1.intfb.h" + #include "subroutine_3.intfb.h" + call subroutine_1() + call subroutine_3() + call subroutine_3() + end subroutine driver_0 +end module driver_0_mod diff --git a/loki/tests/sources/projMultiMode/driver_1_mod.F90 b/loki/tests/sources/projMultiMode/driver_1_mod.F90 new file mode 100644 index 000000000..74d31e6a1 --- /dev/null +++ b/loki/tests/sources/projMultiMode/driver_1_mod.F90 @@ -0,0 +1,12 @@ +module driver_1_mod + implicit none +contains + + subroutine driver_1() + #include "subroutine_1.intfb.h" + #include "subroutine_3.intfb.h" + call subroutine_1() + call subroutine_3() + call subroutine_3() + end subroutine driver_1 +end module driver_1_mod diff --git a/loki/tests/sources/projMultiMode/driver_2_mod.F90 b/loki/tests/sources/projMultiMode/driver_2_mod.F90 new file mode 100644 index 000000000..14a260d1c --- /dev/null +++ b/loki/tests/sources/projMultiMode/driver_2_mod.F90 @@ -0,0 +1,10 @@ +module driver_2_mod + implicit none + +contains + + subroutine driver_2() + #include "subroutine_2.intfb.h" + call subroutine_2() + end subroutine driver_2 +end module driver_2_mod diff --git a/loki/tests/sources/projMultiMode/driver_3_mod.F90 b/loki/tests/sources/projMultiMode/driver_3_mod.F90 new file mode 100644 index 000000000..c27c1c6cf --- /dev/null +++ b/loki/tests/sources/projMultiMode/driver_3_mod.F90 @@ -0,0 +1,10 @@ +module driver_3_mod + implicit none + +contains + + subroutine driver_3() + #include "subroutine_3.intfb.h" + call subroutine_3() + end subroutine driver_3 +end module driver_3_mod diff --git a/loki/tests/sources/projMultiMode/driver_4_mod.F90 b/loki/tests/sources/projMultiMode/driver_4_mod.F90 new file mode 100644 index 000000000..c5f63491a --- /dev/null +++ b/loki/tests/sources/projMultiMode/driver_4_mod.F90 @@ -0,0 +1,10 @@ +module driver_4_mod + implicit none + +contains + + subroutine driver_4() + #include "subroutine_3.intfb.h" + call subroutine_3() + end subroutine driver_4 +end module driver_4_mod diff --git a/loki/tests/sources/projMultiMode/nested_subroutine_1.F90 b/loki/tests/sources/projMultiMode/nested_subroutine_1.F90 new file mode 100644 index 000000000..b2e6ab096 --- /dev/null +++ b/loki/tests/sources/projMultiMode/nested_subroutine_1.F90 @@ -0,0 +1,2 @@ +subroutine nested_subroutine_1() +end subroutine nested_subroutine_1 diff --git a/loki/tests/sources/projMultiMode/nested_subroutine_2.F90 b/loki/tests/sources/projMultiMode/nested_subroutine_2.F90 new file mode 100644 index 000000000..5aced76a4 --- /dev/null +++ b/loki/tests/sources/projMultiMode/nested_subroutine_2.F90 @@ -0,0 +1,2 @@ +subroutine nested_subroutine_2() +end subroutine nested_subroutine_2 diff --git a/loki/tests/sources/projMultiMode/nested_subroutine_3.F90 b/loki/tests/sources/projMultiMode/nested_subroutine_3.F90 new file mode 100644 index 000000000..4fd53c4d8 --- /dev/null +++ b/loki/tests/sources/projMultiMode/nested_subroutine_3.F90 @@ -0,0 +1,2 @@ +subroutine nested_subroutine_3() +end subroutine nested_subroutine_3 diff --git a/loki/tests/sources/projMultiMode/subroutine_1.F90 b/loki/tests/sources/projMultiMode/subroutine_1.F90 new file mode 100644 index 000000000..ad6640c87 --- /dev/null +++ b/loki/tests/sources/projMultiMode/subroutine_1.F90 @@ -0,0 +1,4 @@ +subroutine subroutine_1() + #include "nested_subroutine_1.intfb.h" + call nested_subroutine_1() +end subroutine subroutine_1 diff --git a/loki/tests/sources/projMultiMode/subroutine_2.F90 b/loki/tests/sources/projMultiMode/subroutine_2.F90 new file mode 100644 index 000000000..1377d4109 --- /dev/null +++ b/loki/tests/sources/projMultiMode/subroutine_2.F90 @@ -0,0 +1,6 @@ +subroutine subroutine_2() + #include "nested_subroutine_2.intfb.h" + #include "nested_subroutine_3.intfb.h" + call nested_subroutine_2() + call nested_subroutine_3() +end subroutine subroutine_2 diff --git a/loki/tests/sources/projMultiMode/subroutine_3.F90 b/loki/tests/sources/projMultiMode/subroutine_3.F90 new file mode 100644 index 000000000..4f1e63c51 --- /dev/null +++ b/loki/tests/sources/projMultiMode/subroutine_3.F90 @@ -0,0 +1,12 @@ +subroutine subroutine_3() + #include "nested_subroutine_1.intfb.h" + #include "nested_subroutine_3.intfb.h" + +! INTERFACE +! SUBROUTINE nested_subroutine_3() +! END SUBROUTINE nested_subroutine_3 +! END INTERFACE + + call nested_subroutine_1() + call nested_subroutine_3() +end subroutine subroutine_3 diff --git a/loki/tests/sources/projMultiModeModules/driver_0_mod.F90 b/loki/tests/sources/projMultiModeModules/driver_0_mod.F90 new file mode 100644 index 000000000..6aac6712f --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/driver_0_mod.F90 @@ -0,0 +1,12 @@ +module driver_0_mod + implicit none +contains + + subroutine driver_0() + use subroutine_1_mod, only: subroutine_1 + use subroutine_3_mod, only: subroutine_3 + call subroutine_1() + call subroutine_3() + call subroutine_3() + end subroutine driver_0 +end module driver_0_mod diff --git a/loki/tests/sources/projMultiModeModules/driver_1_mod.F90 b/loki/tests/sources/projMultiModeModules/driver_1_mod.F90 new file mode 100644 index 000000000..105b02151 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/driver_1_mod.F90 @@ -0,0 +1,12 @@ +module driver_1_mod + implicit none +contains + + subroutine driver_1() + use subroutine_1_mod, only: subroutine_1 + use subroutine_3_mod, only: subroutine_3 + call subroutine_1() + call subroutine_3() + call subroutine_3() + end subroutine driver_1 +end module driver_1_mod diff --git a/loki/tests/sources/projMultiModeModules/driver_2_mod.F90 b/loki/tests/sources/projMultiModeModules/driver_2_mod.F90 new file mode 100644 index 000000000..faf39612e --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/driver_2_mod.F90 @@ -0,0 +1,10 @@ +module driver_2_mod + implicit none + +contains + + subroutine driver_2() + use subroutine_2_mod, only: subroutine_2 + call subroutine_2() + end subroutine driver_2 +end module driver_2_mod diff --git a/loki/tests/sources/projMultiModeModules/driver_3_mod.F90 b/loki/tests/sources/projMultiModeModules/driver_3_mod.F90 new file mode 100644 index 000000000..17eb27b8d --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/driver_3_mod.F90 @@ -0,0 +1,10 @@ +module driver_3_mod + implicit none + +contains + + subroutine driver_3() + use subroutine_3_mod, only: subroutine_3 + call subroutine_3() + end subroutine driver_3 +end module driver_3_mod diff --git a/loki/tests/sources/projMultiModeModules/driver_4_mod.F90 b/loki/tests/sources/projMultiModeModules/driver_4_mod.F90 new file mode 100644 index 000000000..2acd0b603 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/driver_4_mod.F90 @@ -0,0 +1,10 @@ +module driver_4_mod + implicit none + +contains + + subroutine driver_4() + use subroutine_3_mod, only: subroutine_3 + call subroutine_3() + end subroutine driver_4 +end module driver_4_mod diff --git a/loki/tests/sources/projMultiModeModules/nested_subroutine_1_mod.F90 b/loki/tests/sources/projMultiModeModules/nested_subroutine_1_mod.F90 new file mode 100644 index 000000000..affa0f4b5 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/nested_subroutine_1_mod.F90 @@ -0,0 +1,6 @@ +module nested_subroutine_1_mod +implicit none +contains +subroutine nested_subroutine_1() +end subroutine nested_subroutine_1 +end module nested_subroutine_1_mod diff --git a/loki/tests/sources/projMultiModeModules/nested_subroutine_2_mod.F90 b/loki/tests/sources/projMultiModeModules/nested_subroutine_2_mod.F90 new file mode 100644 index 000000000..8da27e5b8 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/nested_subroutine_2_mod.F90 @@ -0,0 +1,6 @@ +module nested_subroutine_2_mod +implicit none +contains +subroutine nested_subroutine_2() +end subroutine nested_subroutine_2 +end module nested_subroutine_2_mod diff --git a/loki/tests/sources/projMultiModeModules/nested_subroutine_3_mod.F90 b/loki/tests/sources/projMultiModeModules/nested_subroutine_3_mod.F90 new file mode 100644 index 000000000..391a0da25 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/nested_subroutine_3_mod.F90 @@ -0,0 +1,6 @@ +module nested_subroutine_3_mod +implicit none +contains +subroutine nested_subroutine_3() +end subroutine nested_subroutine_3 +end module nested_subroutine_3_mod diff --git a/loki/tests/sources/projMultiModeModules/subroutine_1_mod.F90 b/loki/tests/sources/projMultiModeModules/subroutine_1_mod.F90 new file mode 100644 index 000000000..f77c9e74f --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/subroutine_1_mod.F90 @@ -0,0 +1,8 @@ +module subroutine_1_mod +implicit none +contains +subroutine subroutine_1() + use nested_subroutine_1_mod, only: nested_subroutine_1 + call nested_subroutine_1() +end subroutine subroutine_1 +end module subroutine_1_mod diff --git a/loki/tests/sources/projMultiModeModules/subroutine_2_mod.F90 b/loki/tests/sources/projMultiModeModules/subroutine_2_mod.F90 new file mode 100644 index 000000000..2c68e7b4f --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/subroutine_2_mod.F90 @@ -0,0 +1,10 @@ +module subroutine_2_mod +implicit none +contains +subroutine subroutine_2() + use nested_subroutine_2_mod, only: nested_subroutine_2 + use nested_subroutine_3_mod, only: nested_subroutine_3 + call nested_subroutine_2() + call nested_subroutine_3() +end subroutine subroutine_2 +end module subroutine_2_mod diff --git a/loki/tests/sources/projMultiModeModules/subroutine_3_mod.F90 b/loki/tests/sources/projMultiModeModules/subroutine_3_mod.F90 new file mode 100644 index 000000000..8de7bdf05 --- /dev/null +++ b/loki/tests/sources/projMultiModeModules/subroutine_3_mod.F90 @@ -0,0 +1,11 @@ +module subroutine_3_mod +implicit none +contains +subroutine subroutine_3() + use nested_subroutine_1_mod, only: nested_subroutine_1 + use nested_subroutine_3_mod, only: nested_subroutine_3 + + call nested_subroutine_1() + call nested_subroutine_3() +end subroutine subroutine_3 +end module subroutine_3_mod diff --git a/loki/transformations/build_system/dependency.py b/loki/transformations/build_system/dependency.py index 10bd103d1..b595d5f60 100644 --- a/loki/transformations/build_system/dependency.py +++ b/loki/transformations/build_system/dependency.py @@ -305,8 +305,14 @@ def rename_imports(self, source, imports, targets=None): calls = {str(c.name).lower() for c in FindNodes(CallStatement).visit(source.body)} calls |= {str(c.name).lower() for c in FindInlineCalls().visit(source.body)} + def replace_last(s, old, new): + index = s.rfind(old) + if index == -1: + return s + return s[:index] + new + s[index + len(old):] + # Import statements still point to unmodified call names - calls = {call.replace(f'{self.suffix.lower()}', '') for call in calls} + calls = {replace_last(call, f'{self.suffix.lower()}', '') for call in calls} call_targets = {call for call in calls if call in as_tuple(targets)} # We go through the IR, as C-imports can be attributed to the body @@ -340,7 +346,6 @@ def rename_imports(self, source, imports, targets=None): im._update(module=new_module_name, symbols=symbols) # TODO: Deal with unqualified blanket imports - if import_map: source.spec = Transformer(import_map).visit(source.spec) diff --git a/loki/transformations/build_system/plan.py b/loki/transformations/build_system/plan.py index 402e62399..762dc8b3b 100644 --- a/loki/transformations/build_system/plan.py +++ b/loki/transformations/build_system/plan.py @@ -101,6 +101,12 @@ def plan_file(self, sourcefile, **kwargs): if source_exists: self.sources_to_transform.setdefault(key,[]).append(sourcepath) if item.replicate: + orig_sourcepath = item.orig_path + orig_source_exists = orig_sourcepath.exists() + if self.rootpath is not None: + orig_sourcepath = orig_sourcepath.resolve().relative_to(self.rootpath) + if orig_source_exists and not source_exists: + self.sources_to_transform.setdefault(key,[]).append(orig_sourcepath) # Add new source file next to the old one self.sources_to_append.setdefault(key,[]).append(newsource) else: @@ -143,18 +149,19 @@ def write_plan(self, filepath): self._write_plan(filepath) # write plan file for each key/target/library - all_targets = self.sources_to_transform | self.sources_to_append | self.sources_to_remove - for target in all_targets: - if target is None: + keys = set(self.sources_to_transform.keys()) | \ + set(self.sources_to_append.keys()) | set(self.sources_to_remove.keys()) + for key in keys: + if key is None: continue with Path(filepath).open('a') as f: - # sanitize target = target, e.g., remove '.' and replace with '_' - sanitized_target = target.replace('.', '_') - s_transform = '\n'.join(f' {s}' for s in self.sources_to_transform.get(target, ())) - f.write(f'set( LOKI_SOURCES_TO_TRANSFORM_{sanitized_target} \n{s_transform}\n )\n') + # sanitize key = target, e.g., remove '.' and replace with '_' + sanitized_key = key.replace('.', '_') + s_transform = '\n'.join(f' {s}' for s in self.sources_to_transform.get(key, ())) + f.write(f'set( LOKI_SOURCES_TO_TRANSFORM_{sanitized_key} \n{s_transform}\n )\n') - s_append = '\n'.join(f' {s}' for s in self.sources_to_append.get(target, ())) - f.write(f'set( LOKI_SOURCES_TO_APPEND_{sanitized_target} \n{s_append}\n )\n') + s_append = '\n'.join(f' {s}' for s in self.sources_to_append.get(key, ())) + f.write(f'set( LOKI_SOURCES_TO_APPEND_{sanitized_key} \n{s_append}\n )\n') - s_remove = '\n'.join(f' {s}' for s in self.sources_to_remove.get(target, ())) - f.write(f'set( LOKI_SOURCES_TO_REMOVE_{sanitized_target} \n{s_remove}\n )\n') + s_remove = '\n'.join(f' {s}' for s in self.sources_to_remove.get(key, ())) + f.write(f'set( LOKI_SOURCES_TO_REMOVE_{sanitized_key} \n{s_remove}\n )\n') diff --git a/loki/transformations/dependency.py b/loki/transformations/dependency.py index f23a7eb92..95ba9c751 100644 --- a/loki/transformations/dependency.py +++ b/loki/transformations/dependency.py @@ -5,11 +5,12 @@ # granted to it by virtue of its status as an intergovernmental organisation # nor does it submit to any jurisdiction. -from loki.batch import Transformation +from loki.batch.transformation import Transformation from loki.ir import nodes as ir, Transformer, FindNodes from loki.tools.util import as_tuple, CaseInsensitiveDict +from loki.subroutine import Subroutine -__all__ = ['DuplicateKernel', 'RemoveKernel'] +__all__ = ['DuplicateKernel', 'RemoveKernel', 'SeparateModesKernel'] class DuplicateKernel(Transformation): @@ -330,3 +331,292 @@ def plan_subroutine(self, routine, **kwargs): item.plan_data['removed_dependencies'] += tuple( child for child in successors if child.local_name in self.remove_kernels ) + +class SeparateModesKernel(Transformation): + """ + Duplicate subroutines which includes the creation of new :any:`Item`s + as well as the addition of the corresponding new dependencies. + + Therefore, this transformation creates a new item and also implements + the relevant routines for dry-run pipeline planning runs. + + Parameters + ---------- + duplicate_kernels : str|tuple|list, optional + Kernel name(s) to be duplicated. + duplicate_suffix : str, optional + Suffix to be used to append the original kernel name(s). + duplicate_module_suffix : str, optional + Suffix to be used to append the original module name(s), + if defined, otherwise `duplicate_suffix` + duplicate_subgraph : bool, optional + Whether or not duplicate the subgraph beneath the kernel(s) + that are duplicated. + """ + + creates_items = True + renames_items = True + reverse_traversal = False + + def __init__(self): + self.mapping = {} + + def _get_new_item_name(self, item, mode): + """ + Get new/duplicated item name, more specifically ``local_name``, + ``scope_name`` and ``new_item_name``. + + Parameters + ---------- + item : :any:`Item` + The item used to derive ``local_name``, + ``scope_name`` and ``new_item_name``. + Returns + ------- + mode : TODO + scope_name : str + New item scope name. + new_item_name : str + New item name. + local_name : str + New item local name. + """ + if mode in self.mapping: + mode_rename = self.mapping[mode] + else: + mode_rename = f'_loki{mode.replace("-", "_")}' + self.mapping[mode] = mode_rename + + # Determine new item name + scope_name = item.scope_name + local_name = f'{item.local_name}{mode_rename}' + if scope_name: + if "_mod" in scope_name: + scope_name = scope_name.replace('_mod', f'_{mode_rename}_mod').replace('__', '_') + else: + scope_name = f'{scope_name}{mode_rename}' + + # Try to get existing item from cache + new_item_name = f'{scope_name or ""}#{local_name}' + return scope_name, local_name, new_item_name + + def _get_or_create_or_rename_item(self, item, mode, item_factory, config): + """ + Get, create or rename item including the scope item if there is a + scope. + + Parameters + ---------- + mode : TODO + item : :any:`Item` + Item to duplicate/to use to derive new item. + item_factory : :any:`ItemFactory` + The :any:`ItemFactory` to use when creating the items. + config : :any:`SchedulerConfig` + The scheduler config to use when instantiating new items. + Returns + ------- + :any:`Item` + Newly created item. + """ + scope_name, local_name, new_item_name = self._get_new_item_name(item, mode) + new_item = item_factory.item_cache.get(new_item_name) + # Try to get an item for the scope or create that first + if new_item is None and scope_name: + scope_item = item_factory.item_cache.get(scope_name) + if scope_item: + scope = scope_item.ir + if local_name not in scope and item.local_name in scope: + # Rename the existing item to the new name + scope[item.local_name].name = local_name + + if local_name in scope: + new_item = item_factory.create_from_ir( + scope[local_name], scope, config=config + ) + # Create new item + if new_item is None: + new_item = as_tuple(item_factory.get_or_create_item_from_item(new_item_name, item, config=config))[0] + return new_item + + @staticmethod + def get_parent_items(item, item_factory): + item_cache = item_factory.item_cache + parent_items = () + if item.scope_name in item_cache: + parent_items += (item_cache[item.scope_name],) + if str(item.source.path) in item_cache: + parent_items += (item_cache[str(item.source.path)],) + return parent_items + + def _create_new_items(self, successors, item_factory, config, item, sub_sgraph, + rename_calls=False, ignore=None): + """ + Create new/duplicated items. + + Parameters + ---------- + successors : tuple + Tuple of :any:`Item`s representing the successor items for which + new/duplicated items are created.. + item_factory : :any:`ItemFactory` + The :any:`ItemFactory` to use when creating the items. + config : :any:`SchedulerConfig` + The scheduler config to use when instantiating new items. + item : :any:`Item` + Starting point/source item from which the successors + originate from + sub_sgraph : :any:`SGraph` + Sgraph (copy) representing the subgraph of the directed + overall graph. + rename_calls : bool, optional + Rename calls/imports in accordance to the duplicated + kernels. + Returns + ------- + tuple + Tuple of newly created items. + """ + ignore = as_tuple(ignore) + new_items = () + removed_items = () + item.trafo_data.setdefault('SeparateModes', {}) + for child in successors: + if child.local_name in ignore: + continue + child.trafo_data.setdefault('SeparateModes', {}) + modes_to_duplicate = sorted(list(set(self._get_item_modes(child)))) + if item.mode in child.trafo_data['SeparateModes']: + new_item = child.trafo_data['SeparateModes'][item.mode] + removed_items += (child,) + new_items += as_tuple(new_item) + item.trafo_data['SeparateModes'][child] = new_item + elif len(modes_to_duplicate) > 1: + mode_to_duplicate = item.mode + new_item = self._get_or_create_or_rename_item(child, mode_to_duplicate, item_factory, config) + new_item.config = child.config.copy() + new_item.config['mode'] = mode_to_duplicate + parent_items = self.get_parent_items(new_item, item_factory) + for parent_item in parent_items: + parent_item.config['mode'] = mode_to_duplicate + child.inherited_mode.discard(mode_to_duplicate) + removed_items += (child,) + new_items += as_tuple(new_item) + item.trafo_data['SeparateModes'][child] = new_item + child.trafo_data['SeparateModes'][mode_to_duplicate] = new_item + # recurse + new_item.plan_data.setdefault('removed_dependencies', ()) + new_item.plan_data.setdefault('additional_dependencies', ()) + new_item_new_items, new_item_removed_items = self._create_new_items(sub_sgraph.successors(child), + item_factory, config, new_item, sub_sgraph) + new_item.plan_data['additional_dependencies'] += new_item_new_items + new_item.plan_data['removed_dependencies'] += new_item_removed_items + if rename_calls: + new_dependencies = CaseInsensitiveDict((new_item.local_name, new_item) + for new_item in new_item_new_items) + new_dependencies = CaseInsensitiveDict((k.local_name, v) for k, v in + new_item.trafo_data.get('SeparateModes', {}).items() if not isinstance(k, str)) + self._adapt_calls_imports(new_item.ir, new_dependencies) + else: + mode_to_duplicate = modes_to_duplicate[0] + child.config['mode'] = item.mode + parent_items = self.get_parent_items(child, item_factory) + for parent_item in parent_items: + parent_item.config['mode'] = mode_to_duplicate + + return tuple(new_items), tuple(removed_items) + + def rename_interfaces(self, intfs, new_dependencies): + for i in intfs: + for routine in i.body: + if isinstance(routine, Subroutine): + if new_dependencies and routine.name.lower() in new_dependencies: + routine.name = new_dependencies[routine.name.lower()].local_name + + def rename_imports(self, imports, new_dependencies=None): + # We go through the IR, as C-imports can be attributed to the body + for im in imports: + if im.c_import: + target_symbol, *suffixes = im.module.lower().split('.', maxsplit=1) + if new_dependencies and target_symbol.lower() in new_dependencies and not 'func.h' in suffixes: + # Modify the the basename of the C-style header import + s = '.'.join(im.module.split('.')[1:]) + im._update(module=f'{new_dependencies[target_symbol].local_name}.{s}') + else: + # Modify module import if it imports any call targets + if new_dependencies and im.symbols and any(s in new_dependencies for s in im.symbols): + relevant_symbol = [s for s in im.symbols if s in new_dependencies][0] + _new_symbol = new_dependencies[relevant_symbol] + new_symbol = _new_symbol.scope_ir.procedure_symbol # local_name + new_module_name = _new_symbol.scope_name + im._update(module=new_module_name, symbols=(new_symbol,)) + # TODO: Deal with unqualified blanket imports + + def _adapt_calls_imports(self, routine, new_dependencies): + call_map = {} + if not new_dependencies: + return + for call in FindNodes(ir.CallStatement).visit(routine.body): + call_name = str(call.name).lower() + if call_name in new_dependencies: + new_item = as_tuple(new_dependencies[call_name])[0] + # new_call_name = (new_item.name).lower() # str(new_item.local_name).lower() + proc_symbol = new_item.ir.procedure_symbol.rescope(scope=routine) + call_map[call] = call.clone(name=proc_symbol) + if call_map: + routine.body = Transformer(call_map).visit(routine.body) + + self.rename_imports(imports=routine.imports, new_dependencies=new_dependencies) + intfs = FindNodes(ir.Interface).visit(routine.spec) + self.rename_interfaces(intfs, new_dependencies=new_dependencies) + + def transform_subroutine(self, routine, **kwargs): + # Create new dependency items + item = kwargs.get('item') + sub_sgraph = kwargs.get('sub_sgraph', None) + successors = sub_sgraph.successors(item) if sub_sgraph is not None else () + ignore = tuple(str(t).lower() for t in as_tuple(kwargs.get('ignore', None))) + + item.plan_data.setdefault('removed_dependencies', ()) + item.plan_data.setdefault('additional_dependencies', ()) + + new_dependencies, removed_dependencies = self._create_new_items( + successors=successors, + item_factory=kwargs.get('item_factory'), + config=kwargs.get('scheduler_config'), + item=kwargs.get('item'), + sub_sgraph=sub_sgraph, + rename_calls=True, ignore=ignore + ) + + item.plan_data['additional_dependencies'] += new_dependencies + item.plan_data['removed_dependencies'] += removed_dependencies + + new_dependencies = CaseInsensitiveDict((k.local_name, v) for k, v in + item.trafo_data.get('SeparateModes', {}).items() if not isinstance(k, str)) + self._adapt_calls_imports(routine, new_dependencies) + + def _get_item_modes(self, item): + inherited_modes = item.inherited_mode + modes_to_duplicate = inherited_modes.copy() if inherited_modes is not None else set() + return modes_to_duplicate + + def plan_subroutine(self, routine, **kwargs): + item = kwargs.get('item') + + sub_sgraph = kwargs.get('sub_sgraph', None) + successors = sub_sgraph.successors(item) if sub_sgraph is not None else () + ignore = tuple(str(t).lower() for t in as_tuple(kwargs.get('ignore', None))) + item.plan_data.setdefault('removed_dependencies', ()) + item.plan_data.setdefault('additional_dependencies', ()) + + additional_dep, removed_dep = self._create_new_items( + successors=successors, + item_factory=kwargs.get('item_factory'), + config=kwargs.get('scheduler_config'), + item=item, + sub_sgraph=sub_sgraph, + ignore=ignore + ) + item.plan_data['additional_dependencies'] += additional_dep + item.plan_data['removed_dependencies'] += removed_dep