Source code for psyclone.domain.common.driver_creator

# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2024-2026, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors: J. Henrichs, Bureau of Meteorology
# Modified: S. Siso, STFC Daresbury Lab

'''This module provides a base class for all domain-specific kernel extraction
implementations.
'''

from abc import abstractmethod
from typing import Optional

from psyclone.core import Signature
from psyclone.line_length import FortLineLength
from psyclone.parse import ModuleManager
from psyclone.psyir.backend.fortran import FortranWriter
from psyclone.psyir.backend.language_writer import LanguageWriter
from psyclone.psyir.frontend.fortran import FortranReader
from psyclone.psyir.nodes import (
    Call, ExtractNode, FileContainer, Literal, Node, Reference, Routine,
    StructureReference)
from psyclone.psyir.symbols import (
    ScalarType, ContainerSymbol, DataTypeSymbol, ImportInterface,
    NoType, RoutineSymbol, DataSymbol, UnsupportedFortranType,
    Symbol, AutomaticInterface, UnresolvedType, SymbolTable)
from psyclone.psyir.tools import ReadWriteInfo


[docs] class DriverCreator: '''This class provides the functionality common to all driver creations. The driver is created as follows: 1. A file container is created and the (empty) driver program is added. 2. All symbols from the `read_kernel_data_mod` are added to the symbol table, and a `psy_data` symbol is created that handles the reading of data from the kernel data file. 3. The command line handler is added to the driver. It is responsible for handling command line parameters for the driver specifying the kernel data file to read (and if nothing is specified to use the default, which is the name used in the extraction without MPI rank attached). 4. A copy of the kernel that is being instrumented is created, so that it can be modified without affecting the actual code creation if PSyclone. 5. The method `verify_and_cleanup_psyir` is called with the copied kernel as parameter. The base implementation checks that no structure accesses are in the code (which are not supported - the structures used by the domain-specific code have already been replaced since the extracted region has been lowered). This can be overwritten by domain-specific implementation. For example, in LFRic calls to `set_dirty` and `set_clean` are removed (since they are irrelevant) first. 6. The read-in code is added, using the read_write information which is used when extracting the data. This ensures that the extraction and driver are matching. 7. The copy of the kernel is added. 8. All modules required by the kernel are imported. 9. A call to `handle_precision_symbols` is made, allowing domain-specific derived classes to add precision related symbols as required (see `lfric_extract_driver_creator.py`), or to replace precision symbols (see `extract_driver_creator.py`). 10. Add the code that will compare the results, i.e. comparing all output variables computed in the kernel with the post values stored in the kernel data file. :param region_name: Suggested region name. ''' # ------------------------------------------------------------------------- def __init__(self, region_name: Optional[tuple[str, str]] = None): self._region_name = region_name # -------------------------------------------------------------------------
[docs] @staticmethod def add_call(program: Routine, name: str, args: list[Node]): '''This function creates a call to the subroutine of the given name, providing the arguments. The call will be added to the program and the corresponding RoutineSymbol to its symbol table (if not already present). :param program: the PSyIR Routine to which any code must be added. :param name: name of the subroutine to call. :param args: list of all arguments for the call. :raises TypeError: if there is a symbol with the specified name defined that is not a RoutineSymbol. ''' if name in program.symbol_table: routine_symbol = program.symbol_table.lookup(name) if not isinstance(routine_symbol, RoutineSymbol): raise TypeError( f"Error creating call to '{name}' - existing symbol is " f"of type '{type(routine_symbol).__name__}', not a " f"'RoutineSymbol'.") else: routine_symbol = RoutineSymbol(name) program.symbol_table.add(routine_symbol) call = Call.create(routine_symbol, args) program.addchild(call)
[docs] @staticmethod def add_read_call(program: Routine, name_lit: Literal, sym: DataSymbol, read_var: str): '''This function creates a call to the subroutine that read fields from the data file. :param program: the PSyIR Routine to which any code must be added. :param name_lit: the name of the field in the data file. :param sym: the symbol to store the read data. :param str read_var: the method name to read the data. ''' # TODO #2898: the test for array can be removed if # `is_allocatable` is supported for non-arrays. if sym.is_array and not sym.datatype.is_allocatable: # In case of a non-allocatable array (e.g. a constant # size array from a module), call the ReadVariable # function that does not require an allocatable field DriverCreator.add_call(program, read_var+"NonAlloc", [name_lit, Reference(sym)]) else: # In case of an allocatable array, call the ReadVariable # function that will also allocate this array. DriverCreator.add_call(program, read_var, [name_lit, Reference(sym)])
# -------------------------------------------------------------------------
[docs] @staticmethod def add_result_tests(program: Routine, output_symbols: list[tuple[Symbol, Symbol]]) -> None: '''Adds tests to check that all output variables have the expected value. :param program: the program to which the tests should be added. :param output_symbols: a list containing all output variables of the executed code. Each entry in the list is a 2-tuple, containing first the symbol that was computed when executing the kernels, and then the symbol containing the expected values that have been read in from a file. ''' module = ContainerSymbol("compare_variables_mod") program.symbol_table.add(module) for compare_func in ["compare", "compare_init", "compare_summary"]: compare_sym = RoutineSymbol(compare_func, NoType(), interface=ImportInterface(module)) program.symbol_table.add(compare_sym) DriverCreator.add_call(program, "compare_init", [Literal(f"{len(output_symbols)}", ScalarType.integer_type())]) # TODO #2083: check if this can be combined with psyad result # comparison. for (sym_computed, sym_read) in output_symbols: lit_name = Literal(sym_computed.name, ScalarType.character_type()) DriverCreator.add_call(program, "compare", [lit_name, Reference(sym_computed), Reference(sym_read)]) DriverCreator.add_call(program, "compare_summary", [])
@staticmethod def _create_output_var_code( name: str, program: Routine, read_var: str, postfix: str, module_name: Optional[str] = None ) -> tuple[Symbol, Symbol]: ''' This function creates all code required for an output variable: 1. It declares (and initialises if necessary) the post variable 2. It reads the '_post' field which stores the expected value of variables at the end of the driver. :param name: the name of original variable (i.e. without _post), which will be looked up as a tag in the symbol table. If index is provided, it is incorporated in the tag using f"{name}_{index}_data". :param program: the PSyIR Routine to which any code must be added. It also contains the symbol table to be used. :param read_var: the readvar method to be used including the name of the PSyData object (e.g. 'psy_data%ReadVar') :param postfix: the postfix to use for the expected output values, which are read from the file. :param module_name: if the variable is part of an external module, this contains the module name from which it is imported. Otherwise, this must either not be specified or an empty string. :returns: a 2-tuple containing the output Symbol after the kernel, and the expected output read from the file. ''' # Obtain the symbol of interest symbol_table = program.symbol_table if module_name: # If a module_name is specified, this indicates that this variable # is imported from an external module. The name of the module will # be appended to the tag used in the extracted kernel file, e.g. # `dummy_var2@dummy_mod`. sym = symbol_table.lookup_with_tag(f"{name}@{module_name}") else: sym = symbol_table.lookup(name) # For each variable, we declare a new variable that stores the expected # value with the same datatype and with the given postfix post_name = sym.name + postfix post_sym = symbol_table.new_symbol(post_name, symbol_type=DataSymbol, datatype=sym.datatype.copy()) if isinstance(post_sym.datatype, UnsupportedFortranType): # Manually update symbol name from Unsupported declarations # pylint: disable=protected-access post_sym.datatype = post_sym.datatype.copy() post_sym.datatype._declaration = \ post_sym.datatype._declaration.replace(sym.name, post_name) # Add a psydata read call with the proper name_tag if module_name: post_tag = f"{name}{postfix}@{module_name}" else: post_tag = f"{name}{postfix}" name_lit = Literal(post_tag, ScalarType.character_type()) DriverCreator.add_read_call(program, name_lit, post_sym, read_var) return (sym, post_sym) def _create_read_in_code( self, program: Routine, psy_data: DataSymbol, original_symtab: SymbolTable, read_write_info: ReadWriteInfo, postfix: str, vars_to_ignore: list[tuple[str, Signature]], ) -> list[tuple[Symbol, Symbol]]: '''This function creates the code that reads in the data file produced during extraction. For each: - input variable, it will declare the symbol and add code that reads in the variable using the PSyData library. - output variable, it will create code to read in the expected value, and at the end compare the driver value with the expected one. :param program: the PSyIR Routine to which any code must be added. :param psy_data: the PSyData symbol to be used. :param original_symtab: this is needed because read_write_info has signatures instead of symbols, and the signature still have to be looked up to retrieve the symbol and then the type. :param read_write_info: information about all input and output parameters. :param postfix: a postfix that is added to a variable name to create the corresponding variable that stores the output value from the kernel data file. :param vars_to_ignore: a list of tuples containing signatures and the container name of variables that were not written to the kernel data file (and as such should not be read in, though they still need to be declared). :returns: all output variables. Each entry is a 2-tuple containing the symbol of the output variable, and the symbol that contains the originally values read from the data file. ''' symbol_table = program.scope.symbol_table read_var = f"{psy_data.name}%ReadVariable" # First handle the input local variables that are read (local variables # do not have a module_name and are guaranteed to be in the symtab when # doing lookups, external variables are handled below). Note that at # the moment we consider all read and/or written as input variables. read_stmts = [] for module_name, signature in read_write_info.all_used_vars_list: if not module_name: orig_sym = original_symtab.lookup(signature[0]) sym = orig_sym.copy() sym.interface = AutomaticInterface() symbol_table.add(sym) # Only add the variable if it is not to be ignored if (module_name, signature) not in vars_to_ignore: name_lit = Literal(str(signature), ScalarType.character_type()) read_stmts.append((name_lit, sym)) # Now do the input external variables. These are done after the locals # so that they match the literal tags of the extracting psy-layer ExtractNode.bring_external_symbols(read_write_info, symbol_table) mod_man = ModuleManager.get() for module_name, signature in read_write_info.all_used_vars_list: # Only add if a variable is not supposed to be ignored if module_name and (module_name, signature) not in vars_to_ignore: mod_info = mod_man.get_module_info(module_name) orig_sym = mod_info.get_symbol(signature[0]) tag = f"{signature[0]}@{module_name}" sym = symbol_table.lookup_with_tag(tag) name_lit = Literal(tag, ScalarType.character_type()) read_stmts.append((name_lit, sym)) for name_lit, sym in read_stmts: self.add_read_call(program, name_lit, sym, read_var) # Finally handle the output variables (these are the ones compared # to a stored _post variable) output_symbols = [] for module_name, signature in read_write_info.write_list: if (module_name, signature) in vars_to_ignore: continue # Find the right symbol for the variable. Note that all variables # in the input and output list have been detected as being used # when the variable accesses were analysed. Therefore, these # variables will already have been declared in the symbol table. if module_name: orig_sym = mod_man.get_module_info(module_name).get_symbol( signature[0]) else: orig_sym = symbol_table.lookup(signature[0]) sym_tuple = self._create_output_var_code( str(signature), program, read_var, postfix, module_name=module_name) output_symbols.append(sym_tuple) return output_symbols @staticmethod def _make_valid_unit_name(name: str) -> str: '''Valid program or routine names are restricted to 63 characters, and no special characters like '-' (which is used when adding invoke and region numbers). :param name: a proposed unit name. :returns: a valid program or routine name with special characters removed and restricted to a length of 63 characters. ''' return name.replace("-", "")[:63]
[docs] @staticmethod def import_modules(program: Routine) -> None: '''This function adds all the import statements required for the actual kernel calls. It finds all calls in the PSyIR tree and checks for calls with a ImportInterface. Any such call will get a ContainerSymbol added for the module, and a RoutineSymbol with an import interface pointing to this module. :param program: the PSyIR Routine to which any code must be added. ''' symtab = program.scope.symbol_table for call in program.walk(Call): routine = call.routine.symbol if not isinstance(routine.interface, ImportInterface): continue if routine.name in symtab: # Symbol has already been added - ignore continue # We need to create a new symbol for the module and the routine # called (the PSyIR backend will then create a suitable import # statement). if routine.interface.container_symbol.name in symtab: module = symtab.lookup(routine.interface.container_symbol.name) else: module = ContainerSymbol( routine.interface.container_symbol.name) symtab.add(module) new_routine_sym = RoutineSymbol(routine.name, UnresolvedType(), interface=ImportInterface(module)) symtab.add(new_routine_sym)
# -------------------------------------------------------------------------
[docs] def verify_and_cleanup_psyir(self, extract_region: Node) -> None: """This method is called to verify that no unsupported language features are used. The base implementation checks for StructureReferences (except for ones used in a call, which some DLSs use, e.g. set_dirty in LFRic) :param extract_region: the node with the extracted region. :raises ValueError: if a StructureReference outside of a call is found. """ # DSLs use StructureReferences in calls (e.g in LFRic # set_dirty etc) must be handled (e.g. removed) by a derived # class first for sref in extract_region.walk(StructureReference): raise ValueError(f"The DriverCreator does not support " f"StructureReferences, any such references " f"in the extraction region should have been " f"flattened by the ExtractNode, but found: " f"'{sref.debug_string()}'")
# -------------------------------------------------------------------------
[docs] @abstractmethod def handle_precision_symbols(self, symbol_table: SymbolTable) -> None: """This method is called to allow DSL-specific changes of type information. E.g. it might replace DSL-specific integer types with generic declarations, or add additional import statements to make the required types available. :param symbol_table: the SymbolTable of the driver. """
# ------------------------------------------------------------------------- def _add_command_line_handler(self, program, psy_data_var, module_name, region_name): ''' This function adds code to handle the command line. For now an alternative filename (to the default one that is hard-coded by the created driver) can be specified, which allows the driver to be used with different files, e.g. several dumps from one run, and/or a separate file from each process. It will also add the code to open the input file using the read_kernel_data routine from the extraction library. :param program: The driver PSyIR. :type program: :py:class:`psyclone.psyir.nodes.Routine` :param psy_data_var: the symbol of the PSyDataExtraction type. :type psy_data_var: :py:class:`psyclone.psyir.symbols.Symbol` :param str module_name: the name of the module, used to create the implicit default kernel dump file name. :param str region_name: the name of the region, used to create the implicit default kernel dump file name. ''' # pylint: disable=too-many-locals program_symbol_table = program.symbol_table # PSyIR does not support allocatable strings, so create the two # variables we need in a loop. # TODO #2137: The UnsupportedFortranType could be reused for all # variables once this is fixed. for str_name in ["psydata_filename", "psydata_arg"]: str_unique_name = \ program_symbol_table.next_available_name(str_name) str_type = UnsupportedFortranType( f"character(:), allocatable :: {str_unique_name}") sym = DataTypeSymbol(str_unique_name, str_type) program_symbol_table.add(sym) if str_name == "psydata_filename": psydata_filename = str_unique_name else: psydata_arg = str_unique_name psydata_len = program_symbol_table.find_or_create( "psydata_len", symbol_type=DataSymbol, datatype=ScalarType.integer_type()).name psydata_i = program_symbol_table.find_or_create( "psydata_i", symbol_type=DataSymbol, datatype=ScalarType.integer_type()).name # We can only parse one statement at a time, so start with the # command line handling: code = f""" do {psydata_i}=1,command_argument_count() call get_command_argument({psydata_i}, length={psydata_len}) allocate(character({psydata_len})::{psydata_arg}) call get_command_argument({psydata_i}, {psydata_arg}, & length={psydata_len}) if ({psydata_arg} == "--update") then ! For later to allow marking fields as being updated else allocate(character({psydata_len})::{psydata_filename}) {psydata_filename} = {psydata_arg} endif deallocate({psydata_arg}) enddo """ command_line = \ FortranReader().psyir_from_statement(code, program_symbol_table) program.children.insert(0, command_line) # Now add the handling of the filename parameter code = f""" if (allocated({psydata_filename})) then call {psy_data_var.name}%OpenReadFileName({psydata_filename}) else call {psy_data_var.name}%OpenReadModuleRegion('{module_name}', & '{region_name}') endif """ filename_test = \ FortranReader().psyir_from_statement(code, program_symbol_table) program.children.insert(1, filename_test) # -------------------------------------------------------------------------
[docs] @staticmethod def collect_all_required_modules( file_container: FileContainer) -> dict[str, set[str]]: '''Collects recursively all modules used in the file container. It returns a dictionary, with the keys being all the (directly or indirectly) used modules. :param file_container: the FileContainer for which to collect all used modules. :returns: a dictionary, with the required module names as key, and as value a set of all modules required by the key module. ''' all_mods: set[str] = set() for container in file_container.children: sym_tab = container.symbol_table # Add all imported modules (i.e. all container symbols) all_mods.update(symbol.name for symbol in sym_tab.symbols if isinstance(symbol, ContainerSymbol)) mod_manager = ModuleManager.get() return mod_manager.get_all_dependencies_recursively( list(all_mods))
# -------------------------------------------------------------------------
[docs] def create(self, nodes: list[Node], read_write_info: ReadWriteInfo, prefix: str, postfix: str, region_name: tuple[str, str], vars_to_ignore: list[tuple[str, Signature]]) -> FileContainer: # pylint: disable=too-many-arguments '''This function uses the PSyIR to create a stand-alone driver that reads in a previously created file with kernel input and output information, and calls the kernels specified in the 'nodes' PSyIR tree with the parameters from the file. The `nodes` are consecutive nodes from the PSyIR tree. It returns the file container which contains the driver. :param nodes: a list of nodes. :param read_write_info: information about all input and output parameters. :param prefix: the prefix to use for each PSyData symbol, e.g. 'extract' as prefix will create symbols ``extract_psydata``. :param postfix: a postfix that is appended to an output variable to create the corresponding variable that stores the output value from the kernel data file. The caller must guarantee that no name clashes are created when adding the postfix to a variable and that the postfix is consistent between extract code and driver code (see 'ExtractTrans.determine_postfix()'). :param region_name: an optional name to use for this PSyData area, provided as a 2-tuple containing a location name followed by a local name. The pair of strings should uniquely identify a region. :param vars_to_ignore: a list of tuples containing signatures and the container name of variables that were not written to the kernel data file (and as such should not be read in, though they still need to be declared). :returns: the program PSyIR for a stand-alone driver. ''' # pylint: disable=too-many-locals module_name, local_name = region_name unit_name = self._make_valid_unit_name(f"{module_name}_{local_name}") # First create the file container, which will only store the program: file_container = FileContainer(unit_name) # Create the program and add it to the file container: program = Routine.create(unit_name, is_program=True) program_symbol_table = program.symbol_table file_container.addchild(program) # Add the extraction library symbols psy_data_mod = ContainerSymbol("read_kernel_data_mod") program_symbol_table.add(psy_data_mod) psy_data_type = DataTypeSymbol("ReadKernelDataType", UnresolvedType(), interface=ImportInterface(psy_data_mod)) program_symbol_table.add(psy_data_type) if prefix: prefix = prefix + "_" root_name = prefix + "psy_data" psy_data = program_symbol_table.new_symbol(root_name=root_name, symbol_type=DataSymbol, datatype=psy_data_type) # Add cmd line handler, read in, and result comparison for the code self._add_command_line_handler(program, psy_data, region_name[0], region_name[1]) original_symbol_table = nodes[0].ancestor(Routine).symbol_table # Add the modified extracted region into the driver program extract_region = nodes[0].copy() self.verify_and_cleanup_psyir(extract_region) output_symbols = self._create_read_in_code(program, psy_data, original_symbol_table, read_write_info, postfix, vars_to_ignore) # Copy the nodes that are part of the extraction program.children.extend(extract_region.pop_all_children()) # Find all imported modules and add them to the symbol table self.import_modules(program) self.handle_precision_symbols(program.scope.symbol_table) self.add_result_tests(program, output_symbols) # Replace pointers with allocatables program_symbol_table = program.symbol_table for symbol in program_symbol_table.datasymbols: if isinstance(symbol.datatype, UnsupportedFortranType): symbol.datatype = symbol.datatype.copy() newt = symbol.datatype.declaration newt = newt.replace('pointer', 'allocatable') newt = newt.replace('=> null()', '') symbol.datatype._declaration = newt return file_container
# -------------------------------------------------------------------------
[docs] def get_driver_as_string(self, nodes: list[Node], read_write_info: ReadWriteInfo, prefix: str, postfix: str, region_name: tuple[str, str], vars_to_ignore: list[tuple[str, Signature]], writer: LanguageWriter) -> str: # pylint: disable=too-many-arguments, too-many-locals '''This function uses the `create()` function to get the PSyIR of a stand-alone driver, and then uses the provided language writer to create a string representation in the selected language. All required modules will be inlined in the correct order, i.e. each module will only depend on modules inlined earlier, which will allow compilation of the driver. No other dependencies (except system dependencies like NetCDF) are required for compilation. :param nodes: a list of nodes. :param read_write_info: information about all input and output parameters. :param prefix: the prefix to use for each PSyData symbol, e.g. 'extract' as prefix will create symbols `extract_psydata`. :param postfix: a postfix that is appended to an output variable to create the corresponding variable that stores the output value from the kernel data file. The caller must guarantee that no name clashes are created when adding the postfix to a variable and that the postfix is consistent between extract code and driver code (see 'ExtractTrans.determine_postfix()'). :param region_name: an optional name to use for this PSyData area, provided as a 2-tuple containing a location name followed by a local name. The pair of strings should uniquely identify a region. :param vars_to_ignore: a list of tuples containing signatures and the container name of variables that were not written to the kernel data file (and as such should not be read in, though they still need to be declared). :param language_writer: a backend visitor to convert PSyIR representation to the selected language. :returns: the driver in the selected language. ''' file_container = self.create(nodes, read_write_info, prefix, postfix, region_name, vars_to_ignore) # Inline all required modules into the driver source file so that # it is stand-alone. module_dependencies = self.collect_all_required_modules(file_container) # Sort the modules by dependencies, i.e. start with modules # that have no dependency. This is required for compilation, the # compiler must have found any dependent modules before it can # compile a module. mod_manager = ModuleManager.get() sorted_modules = mod_manager.sort_modules(module_dependencies) out = [] for module in sorted_modules: # Note that all modules in `sorted_modules` are known to be in # the module manager, so we can always get the module info here. mod_info = mod_manager.get_module_info(module) out.append(mod_info.get_source_code()) out.append(writer(file_container)) return "\n".join(out)
# -------------------------------------------------------------------------
[docs] def write_driver(self, nodes: list[Node], read_write_info: ReadWriteInfo, prefix: str, postfix: str, region_name: tuple[str, str], vars_to_ignore: list[tuple[str, Signature]], writer: Optional[LanguageWriter] = None) -> None: # pylint: disable=too-many-arguments '''This function uses the `get_driver_as_string()` function to get a a stand-alone driver, and then writes this source code to a file. The file name is derived from the region name: "driver-"+module_name+"_"+region_name+".F90" :param nodes: a list of nodes containing the body of the driver routine. :param read_write_info: information about all input and output parameters. :param prefix: the prefix to use for each PSyData symbol, e.g. 'extract' as prefix will create symbols `extract_psydata`. :param postfix: a postfix that is appended to an output variable to create the corresponding variable that stores the output value from the kernel data file. The caller must guarantee that no name clashes are created when adding the postfix to a variable and that the postfix is consistent between extract code and driver code (see 'ExtractTrans.determine_postfix()'). :param region_name: an optional name to use for this PSyData area, provided as a 2-tuple containing a location name followed by a local name. The pair of strings should uniquely identify a region. :param vars_to_ignore: a list of tuples containing signatures and the container name of variables that were not written to the kernel data file (and as such should not be read in, though they still need to be declared). :param writer: a backend visitor to convert PSyIR representation to the selected language. It defaults to the FortranWriter. ''' if self._region_name is not None: region_name = self._region_name if writer is None: writer = FortranWriter() code = self.get_driver_as_string(nodes, read_write_info, prefix, postfix, region_name, vars_to_ignore, writer) fll = FortLineLength() code = fll.process(code) module_name, local_name = region_name with open(f"driver-{module_name}-{local_name}.F90", "w", encoding='utf-8') as out: out.write(code)