diff --git a/changelog b/changelog index fce18d9906..6a3a4b41c1 100644 --- a/changelog +++ b/changelog @@ -57,7 +57,7 @@ 21) PR #2362 for #2243 and #2312. Add support for Fortran save statements. 22) PR #2379 for #2370. Adds a Node.get_sibling_lists() method. - + 23) PR #2366 towards #342. Improve symbols and tighten-up ACCRoutineTrans validation. @@ -77,6 +77,15 @@ 29) PR #2406 for #2404. Fixes bug in setop_random kernel (was only filling the lowest vertical level). + 30) PR #2389 toward #928. Adds support for field_bcs_kernel when + parsing kernels using kernel_interface.py. This is required when + validating LFRic kernel arguments. + + 31) PR #2388 for #2323. Rename the array reduction 2code transformations + to use the 2loop postfix and improve their implementation. + + 32) PR #2400 for #1508. Add flag/config option to disable backend checks. + release 2.4.0 29th of September 2023 1) PR #1758 for #1741. Splits the PSyData read functionality into a diff --git a/doc/developer_guide/psyir_backends.rst b/doc/developer_guide/psyir_backends.rst index 4a515409f2..141c2e809b 100644 --- a/doc/developer_guide/psyir_backends.rst +++ b/doc/developer_guide/psyir_backends.rst @@ -1,7 +1,7 @@ .. ----------------------------------------------------------------------------- BSD 3-Clause License - Copyright (c) 2017-2022, Science and Technology Facilities Council. + Copyright (c) 2017-2023, Science and Technology Facilities Council. All rights reserved. Redistribution and use in source and binary forms, with or without @@ -34,6 +34,7 @@ Authors: R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab +.. _psyir-backends: PSyIR Back-ends ############### diff --git a/doc/user_guide/configuration.rst b/doc/user_guide/configuration.rst index fc1a000c4c..b428c33e8e 100644 --- a/doc/user_guide/configuration.rst +++ b/doc/user_guide/configuration.rst @@ -1,7 +1,7 @@ .. ----------------------------------------------------------------------------- .. BSD 3-Clause License .. -.. Copyright (c) 2018-2022, Science and Technology Facilities Council +.. Copyright (c) 2018-2023, Science and Technology Facilities Council .. All rights reserved. .. .. Redistribution and use in source and binary forms, with or without @@ -49,7 +49,7 @@ is in the ``PSyclone/config`` directory of the PSyclone distribution. At execution-time, the user can specify a custom configuration file to -be used. This can either be done with the ``--config`` command line +be used. This can either be done with the ``--config`` command-line option, or by specifying the (full path to the) configuration file to use via the ``PSYCLONE_CONFIG`` environment variable. If the specified configuration file is not found then PSyclone will fall back to @@ -141,18 +141,20 @@ including "true/false", "yes/no" and "1/0". See https://docs.python.org/3/library/configparser.html#supported-datatypes for more details. +.. _config-default-section: + ``DEFAULT`` Section ^^^^^^^^^^^^^^^^^^^ This section contains entries that are, in principle, applicable to all APIs supported by PSyclone. -.. tabularcolumns:: |l|L| +.. tabularcolumns:: |l|L|l| -======================= ======================================================= -Entry Description -======================= ======================================================= -DEFAULTAPI The API that PSyclone assumes an Algorithm/Kernel +======================= ======================================================= =========== +Entry Description Type +======================= ======================================================= =========== +DEFAULTAPI The API that PSyclone assumes an Algorithm/Kernel str conforms to if no API is specified. Must be one of the APIs supported by PSyclone ("dynamo0.3", "gocean1.0" and "nemo"). If there is no @@ -162,26 +164,30 @@ DEFAULTAPI The API that PSyclone assumes an Algorithm/Kernel line option '-api'. If there is no API entry in the config file, and '-api' is not specified on the command line, "dynamo0.3" is used as default. -DEFAULTSTUBAPI The API that the kernel-stub generator assumes by +DEFAULTSTUBAPI The API that the kernel-stub generator assumes by str default. Must be one of the stub-APIs supported by PSyclone ("dynamo0.3" only at this stage). -DISTRIBUTED_MEMORY Whether or not to generate code for distributed-memory +DISTRIBUTED_MEMORY Whether or not to generate code for distributed-memory bool parallelism by default. Note that this is currently only supported for the LFRic (Dynamo 0.3) API. -REPRODUCIBLE_REDUCTIONS Whether or not to generate code for reproducible OpenMP +REPRODUCIBLE_REDUCTIONS Whether or not to generate code for reproducible OpenMP bool reductions (see :ref:`openmp-reductions`) by default. -REPROD_PAD_SIZE If generating code for reproducible OpenMP reductions, +REPROD_PAD_SIZE If generating code for reproducible OpenMP reductions, int this setting controls the amount of padding used between elements of the array in which each thread accumulates its local reduction. (This prevents false sharing of cache lines by different threads.) -PSYIR_ROOT_NAME The root for generated PSyIR symbol names if one is not +PSYIR_ROOT_NAME The root for generated PSyIR symbol names if one is not str supplied when creating a symbol. Defaults to "psyir_tmp". -VALID_PSY_DATA_PREFIXES Which class prefixes are permitted in any +VALID_PSY_DATA_PREFIXES Which class prefixes are permitted in any list of str PSyData-related transformations. See :ref:`psy_data` for details. -======================= ======================================================= +BACKEND_CHECKS_ENABLED Optional (defaults to True). Whether or not the PSyIR bool + backend should validate the tree that it is passed. + Can be overridden by the ``--backend`` command-line + flag (see :ref:`backend-options`). +======================= ======================================================= =========== Common Sections ^^^^^^^^^^^^^^^ diff --git a/doc/user_guide/psyclone_command.rst b/doc/user_guide/psyclone_command.rst index cd6666b032..7efb3b42b3 100644 --- a/doc/user_guide/psyclone_command.rst +++ b/doc/user_guide/psyclone_command.rst @@ -65,7 +65,9 @@ by the command: usage: psyclone [-h] [-oalg OALG] [-opsy OPSY] [-okern OKERN] [-api API] [-s SCRIPT] [-d DIRECTORY] [-I INCLUDE] [-l {off,all,output}] [-dm] [-nodm] [--kernel-renaming {multiple,single}] - [--profile {invokes,kernels}] [--config CONFIG] [--version] + [--profile {invokes,kernels}] + [--backend {enable-validation,disable-validation}] + [--config CONFIG] [--version] filename Run the PSyclone code generator on a particular file @@ -101,6 +103,10 @@ by the command: kernels --profile {invokes,kernels}, -p {invokes,kernels} Add profiling hooks for either 'kernels' or 'invokes' + --backend {dis,en}able-validation + Options to control the PSyIR backend used for code + generation. Use 'disable-validation' to disable the + validation checks that are performed by default. --config CONFIG Config file with PSyclone specific options. --version, -v Display version information (\ |release|\ ) @@ -379,6 +385,30 @@ by the ``-I``/``--include`` flags. (Currently this search assumes that a module named e.g. "my_mod" will be in a file named "my_mod.*90" - see issue #1895.) +.. _backend-options: + +Backend Options +--------------- + +The final code generated by PSyclone is created by passing the PSyIR +tree to one of the 'backends' (see :ref:`dev_guide:psyir-backends` in +the Developer Guide for more details). The ``--backend`` flag permits +a user to tune the behaviour of this code generation. Currently, the +only option is ``{en,dis}able-validation`` which turns on/off the +validation checks performed when doing code generation. By default, +such validation is enabled as it is only at code-generation time that +certain constraints can be checked (since PSyclone does not mandate +the order in which code transformations are applied). Occasionally, +these validation checks may raise false positives (due to incomplete +implementations), at which point it is useful to be able to disable +them. The default behaviour may be changed by adding the +``BACKEND_CHECKS_ENABLED`` entry to the +:ref:`configuration file `. Any +command-line setting always takes precendence though. It is +recommended that validation only be disabled as a last resort and for +as few input source files as possible. + + C Pre-processor #include Files ------------------------------ diff --git a/doc/user_guide/psyir.rst b/doc/user_guide/psyir.rst index 77e23645ed..3ab4bcb788 100644 --- a/doc/user_guide/psyir.rst +++ b/doc/user_guide/psyir.rst @@ -67,13 +67,12 @@ collectively as 'PSyIR nodes'. At the present time PSyIR classes can be essentially split into two types. PSy-layer classes and Kernel-layer classes. PSy-layer classes -make use of a ``gen_code()`` or an ``update()`` method to create -Fortran code whereas Kernel-layer classes make use of PSyIR backends -to create code. +make use of a ``gen_code()`` method to create Fortran code whereas +Kernel-layer classes make use of PSyIR backends to create code. .. note:: This separation will be removed in the future and eventually all PSyIR classes will make use of backends with the - expectation that ``gen_code()`` and ``update()`` methods + expectation that ``gen_code()`` methods will be removed. Further this separation will be superseded by a separation between ``language-level PSyIR`` and ``domain-specific PSyIR``. @@ -229,7 +228,7 @@ Following the `parent` and `children` terminology, we define a node's `siblings` as the children of its parent. Note that this definition implies that all nodes are their own siblings. -.. automethod:: psyclone.psyir.nodes.Node.siblings +.. autoproperty:: psyclone.psyir.nodes.Node.siblings We can check whether two nodes are siblings which immediately precede or follow one another using the following methods: diff --git a/doc/user_guide/transformations.rst b/doc/user_guide/transformations.rst index 46805a3461..2279b6e0ba 100644 --- a/doc/user_guide/transformations.rst +++ b/doc/user_guide/transformations.rst @@ -282,7 +282,7 @@ can be found in the API-specific sections). #### -.. autoclass:: psyclone.psyir.transformations.Maxval2CodeTrans +.. autoclass:: psyclone.psyir.transformations.Maxval2LoopTrans :members: apply :noindex: @@ -299,7 +299,7 @@ can be found in the API-specific sections). #### -.. autoclass:: psyclone.psyir.transformations.Minval2CodeTrans +.. autoclass:: psyclone.psyir.transformations.Minval2LoopTrans :members: apply :noindex: @@ -412,6 +412,12 @@ can be found in the API-specific sections). #### +.. autoclass:: psyclone.psyir.transformations.Product2LoopTrans + :members: apply + :noindex: + +#### + .. autoclass:: psyclone.psyir.transformations.ProfileTrans :members: apply :noindex: @@ -449,7 +455,7 @@ can be found in the API-specific sections). #### -.. autoclass:: psyclone.psyir.transformations.Sum2CodeTrans +.. autoclass:: psyclone.psyir.transformations.Sum2LoopTrans :members: apply :noindex: diff --git a/psyclone.pdf b/psyclone.pdf index 2db0956c62..6f5fbee282 100644 Binary files a/psyclone.pdf and b/psyclone.pdf differ diff --git a/src/psyclone/configuration.py b/src/psyclone/configuration.py index 50622c4b4d..002a7e0d9a 100644 --- a/src/psyclone/configuration.py +++ b/src/psyclone/configuration.py @@ -223,6 +223,11 @@ def __init__(self): # Number of OpenCL devices per node self._ocl_devices_per_node = 1 + # By default, a PSyIR backend performs validation checks as it + # traverses the tree. Setting this option to False disables those + # checks which can be useful in the case of unimplemented features. + self._backend_checks_enabled = True + # ------------------------------------------------------------------------- def load(self, config_file=None): '''Loads a configuration file. @@ -358,6 +363,17 @@ def load(self, config_file=None): f"prefix must be valid for use as the start of a Fortran " f"variable name.", config=self) + # Whether validation is performed in the PSyIR backends. + if 'BACKEND_CHECKS_ENABLED' in self._config['DEFAULT']: + try: + self._backend_checks_enabled = ( + self._config['DEFAULT'].getboolean( + 'BACKEND_CHECKS_ENABLED')) + except ValueError as err: + raise ConfigurationError( + f"Error while parsing BACKEND_CHECKS_ENABLED: {err}", + config=self) from err + # Now we deal with the API-specific sections of the config file. We # create a dictionary to hold the API-specific Config objects. self._api_conf = {} @@ -547,6 +563,31 @@ def default_stub_api(self): ''' return self._default_stub_api + @property + def backend_checks_enabled(self): + ''' + :returns: whether the validity checks in the PSyIR backend should be + disabled. + :rtype: bool + ''' + return self._backend_checks_enabled + + @backend_checks_enabled.setter + def backend_checks_enabled(self, value): + ''' + Setter for whether or not the PSyIR backend is to perform validation + checks. + + :param bool value: whether or not to perform validation. + + :raises TypeError: if `value` is not a boolean. + + ''' + if not isinstance(value, bool): + raise TypeError(f"Config.backend_checks_enabled must be a boolean " + f"but got '{type(value).__name__}'") + self._backend_checks_enabled = value + @property def supported_stub_apis(self): ''' diff --git a/src/psyclone/domain/lfric/arg_ordering.py b/src/psyclone/domain/lfric/arg_ordering.py index 056f5141be..d22fa34bfe 100644 --- a/src/psyclone/domain/lfric/arg_ordering.py +++ b/src/psyclone/domain/lfric/arg_ordering.py @@ -47,6 +47,8 @@ # a circular dependency. from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.lfric_symbol_table import LFRicSymbolTable +from psyclone.domain.lfric.metadata_to_arguments_rules import ( + MetadataToArgumentsRules) from psyclone.errors import GenerationError, InternalError from psyclone.psyir.nodes import ArrayReference, Reference from psyclone.psyir.symbols import ScalarType @@ -472,8 +474,8 @@ def generate(self, var_accesses=None): self.diff_basis(unique_fs, var_accesses=var_accesses) # Fix for boundary_dofs array to the boundary condition # kernel (enforce_bc_kernel) arguments - if self._kern.name.lower() == "enforce_bc_code" and \ - unique_fs.orig_name.lower() == "any_space_1": + if (MetadataToArgumentsRules.bc_kern_regex.match(self._kern.name) + and unique_fs.orig_name.lower() == "any_space_1"): self.field_bcs_kernel(unique_fs, var_accesses=var_accesses) # Add boundary dofs array to the operator boundary condition diff --git a/src/psyclone/domain/lfric/kernel_interface.py b/src/psyclone/domain/lfric/kernel_interface.py index 2c47d994f9..42af26cbb7 100644 --- a/src/psyclone/domain/lfric/kernel_interface.py +++ b/src/psyclone/domain/lfric/kernel_interface.py @@ -586,20 +586,54 @@ def diff_basis(self, function_space, var_accesses=None): basis_name_func, first_dim_value_func) def field_bcs_kernel(self, function_space, var_accesses=None): - '''Not implemented. + '''Create the boundary-dofs mask argument required for the + enforce_bc_code kernel. Adds it to the symbol table and the argument + list. :param function_space: the function space for this boundary condition. :type function_space: :py:class:`psyclone.domain.lfric.FunctionSpace` - :param var_accesses: an unused optional argument that stores \ + :param var_accesses: an unused optional argument that stores information about variable accesses. - :type var_accesses: :\ - py:class:`psyclone.core.VariablesAccessInfo` + :type var_accesses: :py:class:`psyclone.core.VariablesAccessInfo` - :raises NotImplementedError: as this method is not implemented. + :raises InternalError: if the kernel does not have a single field as + argument. + :raises InternalError: if the field argument is not on the + 'ANY_SPACE_1' function space. ''' - raise NotImplementedError( - "TODO #928: field_bcs_kernel not implemented") + # Sanity check - expect the enforce_bc_code to have a single argument + # that is a field. + if (len(self._kern.arguments.args) != 1 or + not self._kern.arguments.args[0].is_field): + const = LFRicConstants() + raise InternalError( + f"Kernel '{self._kern.name}' applies boundary conditions to a " + f"field and therefore should have a single, field argument " + f"(one of {const.VALID_FIELD_NAMES}) but got " + f"{[arg.argument_type for arg in self._kern.arguments.args]}") + farg = self._kern.arguments.args[0] + fspace = farg.function_space + # Sanity check - expect the field argument to the enforce_bc_code + # kernel to be on the ANY_SPACE_1 space. + if fspace.orig_name != "any_space_1": + raise InternalError( + f"Kernel '{self._kern.name}' applies boundary conditions to a " + f"field but the supplied argument, '{farg.name}', is on " + f"'{fspace.orig_name}' rather than the expected 'ANY_SPACE_1'") + + ndf_symbol = self._symtab.find_or_create_tag( + f"ndf_{fspace.orig_name}", fs=fspace.orig_name, + symbol_type=LFRicTypes("NumberOfDofsDataSymbol"), + interface=self._read_access) + + # Add the boundary-dofs array argument. + sym = self._symtab.find_or_create_tag( + f"boundary_dofs_{farg.name}", + interface=self._read_access, + symbol_type=LFRicTypes("VerticalBoundaryDofMaskDataSymbol"), + dims=[Reference(ndf_symbol), 2]) + self._arglist.append(sym) def operator_bcs_kernel(self, function_space, var_accesses=None): '''Not implemented. diff --git a/src/psyclone/domain/lfric/lfric_types.py b/src/psyclone/domain/lfric/lfric_types.py index fcb0ae5cc2..9a22bdfe8e 100644 --- a/src/psyclone/domain/lfric/lfric_types.py +++ b/src/psyclone/domain/lfric/lfric_types.py @@ -31,13 +31,15 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- -# Author: J. Henrichs, Bureau of Meteorology -# Modified: O. Brunt, Met Office +# Authors: J. Henrichs, Bureau of Meteorology +# A. R. Porter, STFC Daresbury Lab +# O. Brunt, Met Office '''This module contains a singleton class that manages LFRic types. ''' from collections import namedtuple +from dataclasses import dataclass from psyclone.configuration import Config from psyclone.domain.lfric.lfric_constants import LFRicConstants @@ -126,6 +128,7 @@ def init(): LFRicTypes._create_lfric_dimension() LFRicTypes._create_specific_scalars() LFRicTypes._create_fields() + # Generate LFRic vector-field-data symbols as subclasses of # field-data symbols const = LFRicConstants() @@ -271,30 +274,30 @@ def _create_lfric_dimension(): # The actual class: class LFRicDimension(Literal): '''An LFRic-specific scalar integer that captures a literal array - dimension which can either have the value 1 or 3. This is used for - one of the dimensions in basis and differential basis - functions. + dimension which can have a value between 1 and 3, inclusive. This + is used for one of the dimensions in basis and differential basis + functions and also for the vertical-boundary dofs mask. :param str value: the value of the scalar integer. - :raises ValueError: if the supplied value is not '1 or '3'. + :raises ValueError: if the supplied value is not '1', '2' or '3'. ''' # pylint: disable=undefined-variable def __init__(self, value): super().__init__(value, LFRicTypes("LFRicIntegerScalarDataType")()) - if value not in ['1', '3']: - raise ValueError(f"An LFRic dimension object must be '1' " - f"or '3', but found '{value}'.") + if value not in ['1', '2', '3']: + raise ValueError(f"An LFRic dimension object must be '1', " + f"'2' or '3', but found '{value}'.") # -------------------------------------------------------------------- # Create the required entries in the dictionary - LFRicTypes._name_to_class["LFRicDimension"] = LFRicDimension - LFRicTypes._name_to_class["LFRIC_SCALAR_DIMENSION"] = \ - LFRicDimension("1") - LFRicTypes._name_to_class["LFRIC_VECTOR_DIMENSION"] = \ - LFRicDimension("3") + LFRicTypes._name_to_class.update({ + "LFRicDimension": LFRicDimension, + "LFRIC_SCALAR_DIMENSION": LFRicDimension("1"), + "LFRIC_VERTICAL_BOUNDARIES_DIMENSION": LFRicDimension("2"), + "LFRIC_VECTOR_DIMENSION": LFRicDimension("3")}) # ------------------------------------------------------------------------ @staticmethod @@ -404,14 +407,24 @@ def _create_fields(): # list because they are used to create vector field datatypes and # symbols. - # The Array namedtuple has 4 properties: the first determines the - # names of the resultant datatype and datasymbol classes, the second - # references the generic scalar type classes declared above, the third - # property is a textual description of each of the dimensions. - # The fourth specifies any additional class properties that should be - # declared in the generated datasymbol class. - Array = namedtuple('Array', - ["name", "scalar_type", "dims", "properties"]) + @dataclass(frozen=True) + class Array: + ''' + Holds the properties of an LFRic array type, used when generating + DataSymbol and DataSymbolType classes. + + :param name: the base name to use for the datatype and datasymbol. + :param scalar_type: the name of the LFRic scalar type that this is + an array of. + :param dims: textual description of each of the dimensions. + :param properties: names of additional class properties that should + be declared in the generated datasymbol class. + ''' + name: str + scalar_type: str + dims: list # list[str] notation supported in Python 3.9+ + properties: list # ditto + field_datatypes = [ Array("RealField", "LFRicRealScalarDataType", ["number of unique dofs"], ["fs"]), @@ -471,7 +484,9 @@ def _create_fields(): Array("QrWeightsInFaces", "LFRicRealScalarDataType", ["number of qr points in faces"], []), Array("QrWeightsInEdges", "LFRicRealScalarDataType", - ["number of qr points in edges"], []) + ["number of qr points in edges"], []), + Array("VerticalBoundaryDofMask", "LFRicIntegerScalarDataType", + ["number of dofs", LFRicTypes("LFRicDimension")], []) ] for array_type in array_datatypes + field_datatypes: diff --git a/src/psyclone/domain/lfric/metadata_to_arguments_rules.py b/src/psyclone/domain/lfric/metadata_to_arguments_rules.py index 989cbc0d6c..e03b5ba7bf 100644 --- a/src/psyclone/domain/lfric/metadata_to_arguments_rules.py +++ b/src/psyclone/domain/lfric/metadata_to_arguments_rules.py @@ -38,6 +38,7 @@ ''' from collections import OrderedDict +import re from psyclone.domain.lfric import LFRicConstants from psyclone.domain.lfric.kernel import ( @@ -67,6 +68,12 @@ class MetadataToArgumentsRules(): ''' _metadata = None _info = None + # Regex used to identify the special 'enforce_bc_code' kernel that + # applies boundary conditions to a field. Allows for the renaming + # performed by PSyclone when performing kernel transformations. + # TODO #487 - this can be removed when we have metadata to specify + # that a kernel applies boundary conditinos. + bc_kern_regex = re.compile(r"enforce_bc_(\d+_)?code", flags=re.I) @classmethod def mapping(cls, metadata, info=None): @@ -538,8 +545,8 @@ def _generate(cls): # The boundary condition kernel (enforce_bc_kernel) is a # special case. - if cls._metadata.procedure_name and \ - cls._metadata.procedure_name.lower() == "enforce_bc_code": + if (cls._metadata.procedure_name and + cls.bc_kern_regex.match(cls._metadata.procedure_name)): cls._field_bcs_kernel() # The operator boundary condition kernel diff --git a/src/psyclone/domain/nemo/transformations/nemo_arrayrange2loop_trans.py b/src/psyclone/domain/nemo/transformations/nemo_arrayrange2loop_trans.py index bdf291a343..f4a68a1c5d 100644 --- a/src/psyclone/domain/nemo/transformations/nemo_arrayrange2loop_trans.py +++ b/src/psyclone/domain/nemo/transformations/nemo_arrayrange2loop_trans.py @@ -307,7 +307,7 @@ def validate(self, node, options=None): # Is the Range node the outermost Range (as if not, the # transformation would be invalid)? - for child in node.parent.indices[node.parent.indices.index(node)+1:]: + for child in node.parent.indices[node.position+1:]: if isinstance(child, Range): raise TransformationError( "Error in NemoArrayRange2LoopTrans transformation. This " diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py index b1bdbf3125..8ea64d064f 100644 --- a/src/psyclone/dynamo0p3.py +++ b/src/psyclone/dynamo0p3.py @@ -4480,8 +4480,11 @@ def __init__(self, node): # Check through all the kernel calls to see whether any of them # require boundary conditions. Currently this is done by recognising # the kernel name. + # pylint: disable=import-outside-toplevel + from psyclone.domain.lfric.metadata_to_arguments_rules import ( + MetadataToArgumentsRules) for call in self._calls: - if call.name.lower() == "enforce_bc_code": + if MetadataToArgumentsRules.bc_kern_regex.match(call.name): bc_fs = None for fspace in call.arguments.unique_fss: if fspace.orig_name == "any_space_1": diff --git a/src/psyclone/f2pygen.py b/src/psyclone/f2pygen.py index f3be69d4c5..f0d920ed84 100644 --- a/src/psyclone/f2pygen.py +++ b/src/psyclone/f2pygen.py @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BSD 3-Clause License # -# Copyright (c) 2017-2022 and Technology Facilities Council. +# Copyright (c) 2017-2023 and Technology Facilities Council. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -37,7 +37,6 @@ ''' Fortran code-generation library. This wraps the f2py fortran parser to provide routines which can be used to generate fortran code. ''' -from __future__ import absolute_import, print_function import abc from fparser.common.readfortran import FortranStringReader from fparser.common.sourceinfo import FortranFormat @@ -48,6 +47,7 @@ # cannot be used for imports (as that involves looking for the # specified name in sys.modules). from fparser import one as fparser1 +from psyclone.configuration import Config from psyclone.errors import InternalError # Module-wide utility methods @@ -547,11 +547,15 @@ def __init__(self, parent, content): # Import FortranWriter here to avoid circular-dependency # pylint: disable=import-outside-toplevel from psyclone.psyir.backend.fortran import FortranWriter + # We need the Config object in order to see whether or not to disable + # the validation performed in the PSyIR backend. + config = Config.get() # Use the PSyIR Fortran backend to generate Fortran code of the # supplied PSyIR tree and pass the resulting code to the fparser1 # Fortran parser. - fortran_writer = FortranWriter() + fortran_writer = FortranWriter( + check_global_constraints=config.backend_checks_enabled) reader = FortranStringReader(fortran_writer(content), ignore_comments=False) # Set reader as free form, strict diff --git a/src/psyclone/generator.py b/src/psyclone/generator.py index 3d4bba1b77..05d4dcb398 100644 --- a/src/psyclone/generator.py +++ b/src/psyclone/generator.py @@ -476,6 +476,12 @@ def main(args): parser.add_argument( '--profile', '-p', action="append", choices=Profiler.SUPPORTED_OPTIONS, help="Add profiling hooks for either 'kernels' or 'invokes'") + parser.add_argument( + '--backend', dest='backend', + choices=['enable-validation', 'disable-validation'], + help=("Options to control the PSyIR backend used for code generation. " + "Use 'disable-validation' to disable the validation checks that " + "are performed by default.")) parser.set_defaults(dist_mem=Config.get().distributed_memory) parser.add_argument("--config", help="Config file with " @@ -526,6 +532,12 @@ def main(args): api = args.api Config.get().api = api + if args.backend: + # A command-line flag overrides the setting in the Config file (if + # any). + Config.get().backend_checks_enabled = ( + str(args.backend) == "enable-validation") + # The Configuration manager checks that the supplied path(s) is/are # valid so protect with a try try: diff --git a/src/psyclone/nemo.py b/src/psyclone/nemo.py index 98a445afc7..6161d4f52a 100644 --- a/src/psyclone/nemo.py +++ b/src/psyclone/nemo.py @@ -152,7 +152,8 @@ def gen(self): :rtype: str ''' - fwriter = FortranWriter() + enable_checks = Config.get().backend_checks_enabled + fwriter = FortranWriter(check_global_constraints=enable_checks) return fwriter(self._container) diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index ab78c24560..9d2c592d70 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -41,6 +41,7 @@ and generation. The classes in this method need to be specialised for a particular API and implementation. ''' +import os from collections import OrderedDict import abc @@ -1662,9 +1663,10 @@ def rename_and_write(self): is also flagged for module-inlining. ''' - import os from psyclone.line_length import FortLineLength + config = Config.get() + # If this kernel has not been transformed we do nothing, also if the # kernel has been module-inlined, the routine already exist in the # PSyIR and we don't need to generate a new file with it. @@ -1696,12 +1698,12 @@ def rename_and_write(self): # Atomically attempt to open the new kernel file (in case # this is part of a parallel build) fdesc = os.open( - os.path.join(Config.get().kernel_output_dir, new_name), + os.path.join(config.kernel_output_dir, new_name), os.O_CREAT | os.O_WRONLY | os.O_EXCL) except (OSError, IOError): # The os.O_CREATE and os.O_EXCL flags in combination mean # that open() raises an error if the file exists - if Config.get().kernel_naming == "single": + if config.kernel_naming == "single": # If the kernel-renaming scheme is such that we only ever # create one copy of a transformed kernel then we're done break @@ -1718,7 +1720,8 @@ def rename_and_write(self): # file using a PSyIR back-end. At the moment there is no way to choose # which back-end to use, so simply use the Fortran one (and limit the # line length). - fortran_writer = FortranWriter() + fortran_writer = FortranWriter( + check_global_constraints=config.backend_checks_enabled) # Start from the root of the schedule as we want to output # any module information surrounding the kernel subroutine # as well as the subroutine itself. @@ -1731,7 +1734,7 @@ def rename_and_write(self): # because the file already exists and the kernel-naming scheme # ("single") means we're not creating a new one. # Check that what we've got is the same as what's in the file - with open(os.path.join(Config.get().kernel_output_dir, + with open(os.path.join(config.kernel_output_dir, new_name), "r") as ffile: kern_code = ffile.read() if kern_code != new_kern_code: @@ -1739,10 +1742,10 @@ def rename_and_write(self): f"A transformed version of this Kernel " f"'{self._module_name + '''.f90'''}' already exists " f"in the kernel-output directory " - f"({Config.get().kernel_output_dir}) but is not the " + f"({config.kernel_output_dir}) but is not the " f"same as the current, transformed kernel and the " f"kernel-renaming scheme is set to " - f"'{Config.get().kernel_naming}'. (If you wish to" + f"'{config.kernel_naming}'. (If you wish to" f" generate a new, unique kernel for every kernel " f"that is transformed then use " f"'--kernel-renaming multiple'.)") diff --git a/src/psyclone/psyir/nodes/acc_directives.py b/src/psyclone/psyir/nodes/acc_directives.py index b2a36c22ed..a92536c638 100644 --- a/src/psyclone/psyir/nodes/acc_directives.py +++ b/src/psyclone/psyir/nodes/acc_directives.py @@ -518,18 +518,21 @@ def validate_global_constraints(self): Perform validation of those global constraints that can only be done at code-generation time. - :raises GenerationError: if this ACCLoopDirective is not enclosed \ - within some OpenACC parallel or kernels region. - ''' - # It is only at the point of code generation that we can check for - # correctness (given that we don't mandate the order that a user can - # apply transformations to the code). As an orphaned loop directive, - # we must have an ACCParallelDirective or an ACCKernelsDirective as - # an ancestor somewhere back up the tree. - if not self.ancestor((ACCParallelDirective, ACCKernelsDirective)): + :raises GenerationError: if this ACCLoopDirective is not enclosed + within some OpenACC parallel or kernels region and is not in a + Routine that has been marked up with an 'ACC Routine' directive. + ''' + parent_routine = self.ancestor(Routine) + if not (self.ancestor((ACCParallelDirective, ACCKernelsDirective), + limit=parent_routine) or + (parent_routine and parent_routine.walk(ACCRoutineDirective))): + location = (f"in routine '{parent_routine.name}' " if + parent_routine else "") raise GenerationError( - "ACCLoopDirective must have an ACCParallelDirective or " - "ACCKernelsDirective as an ancestor in the Schedule") + f"ACCLoopDirective {location}must either have an " + f"ACCParallelDirective or ACCKernelsDirective as an ancestor " + f"in the Schedule or the routine must contain an " + f"ACCRoutineDirective.") super().validate_global_constraints() diff --git a/src/psyclone/psyir/transformations/__init__.py b/src/psyclone/psyir/transformations/__init__.py index 840487701c..e950623a51 100644 --- a/src/psyclone/psyir/transformations/__init__.py +++ b/src/psyclone/psyir/transformations/__init__.py @@ -66,16 +66,16 @@ Matmul2CodeTrans from psyclone.psyir.transformations.intrinsics.max2code_trans import \ Max2CodeTrans -from psyclone.psyir.transformations.intrinsics.maxval2code_trans import \ - Maxval2CodeTrans +from psyclone.psyir.transformations.intrinsics.maxval2loop_trans import \ + Maxval2LoopTrans from psyclone.psyir.transformations.intrinsics.min2code_trans import \ Min2CodeTrans -from psyclone.psyir.transformations.intrinsics.minval2code_trans import \ - Minval2CodeTrans +from psyclone.psyir.transformations.intrinsics.minval2loop_trans import \ + Minval2LoopTrans from psyclone.psyir.transformations.intrinsics.sign2code_trans import \ Sign2CodeTrans -from psyclone.psyir.transformations.intrinsics.sum2code_trans import \ - Sum2CodeTrans +from psyclone.psyir.transformations.intrinsics.sum2loop_trans import \ + Sum2LoopTrans from psyclone.psyir.transformations.loop_fuse_trans import LoopFuseTrans from psyclone.psyir.transformations.loop_swap_trans import LoopSwapTrans from psyclone.psyir.transformations.loop_tiling_2d_trans \ @@ -88,6 +88,8 @@ from psyclone.psyir.transformations.omp_task_trans import OMPTaskTrans from psyclone.psyir.transformations.parallel_loop_trans import \ ParallelLoopTrans +from psyclone.psyir.transformations.intrinsics.product2loop_trans import \ + Product2LoopTrans from psyclone.psyir.transformations.profile_trans import ProfileTrans from psyclone.psyir.transformations.psy_data_trans import PSyDataTrans from psyclone.psyir.transformations.read_only_verify_trans \ @@ -115,17 +117,20 @@ 'Max2CodeTrans', 'Min2CodeTrans', 'Sign2CodeTrans', - 'Sum2CodeTrans', + 'Sum2LoopTrans', 'LoopFuseTrans', 'LoopSwapTrans', 'LoopTiling2DTrans', 'LoopTrans', + 'Maxval2LoopTrans', + 'Minval2LoopTrans', 'NanTestTrans', 'OMPLoopTrans', 'OMPTargetTrans', 'OMPTaskTrans', 'OMPTaskwaitTrans', 'ParallelLoopTrans', + 'Product2LoopTrans', 'ProfileTrans', 'PSyDataTrans', 'ReadOnlyVerifyTrans', diff --git a/src/psyclone/psyir/transformations/intrinsics/array_reduction_base_trans.py b/src/psyclone/psyir/transformations/intrinsics/array_reduction_base_trans.py new file mode 100644 index 0000000000..6983509688 --- /dev/null +++ b/src/psyclone/psyir/transformations/intrinsics/array_reduction_base_trans.py @@ -0,0 +1,372 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023, 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. +# ----------------------------------------------------------------------------- +# Author: R. W. Ford, STFC Daresbury Lab +# Modified: S. Siso, STFC Daresbury Lab + +'''Module providing common functionality to transformation from a +PSyIR array-reduction intrinsic to PSyIR code. + +''' +from abc import ABC, abstractmethod + +from psyclone.psyir.nodes import ( + Assignment, Reference, ArrayReference, IfBlock, + IntrinsicCall, Node, UnaryOperation, BinaryOperation) +from psyclone.psyir.symbols import ArrayType, DataSymbol +from psyclone.psyGen import Transformation +from psyclone.psyir.transformations.reference2arrayrange_trans import \ + Reference2ArrayRangeTrans +from psyclone.psyir.transformations.transformation_error import \ + TransformationError + + +class ArrayReductionBaseTrans(Transformation, ABC): + '''An abstract parent class providing common functionality to + array-reduction intrinsic transformations which translate the + intrinsics into an equivalent loop structure. + + ''' + _INTRINSIC_NAME = None + _INTRINSIC_TYPE = None + + @staticmethod + def _get_args(node): + '''Utility method that returns the array-reduction intrinsic arguments + (array reference, dimension and mask). + + :param node: an array-reduction intrinsic. + :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` + + returns: a tuple containing the 3 arguments. + rtype: Tuple[py:class:`psyclone.psyir.nodes.reference.Reference`, + py:class:`psyclone.psyir.nodes.Literal` | + :py:class:`psyclone.psyir.nodes.Reference`, + Optional[:py:class:`psyclone.psyir.nodes.Node`]] + + ''' + # Determine the arguments to the intrinsic + args = [None, None, None] + arg_names_map = {"array": 0, "dim": 1, "mask": 2} + for idx, child in enumerate(node.children): + if not node.argument_names[idx]: + # positional arg + args[idx] = child + else: + # named arg + name = node.argument_names[idx].lower() + args[arg_names_map[name]] = child + return tuple(args) + + def __str__(self): + return (f"Convert the PSyIR {self._INTRINSIC_NAME} intrinsic " + "to equivalent PSyIR code.") + + # pylint: disable=too-many-branches + def validate(self, node, options=None): + '''Check that the input node is valid before applying the + transformation. + + :param node: an array-reduction intrinsic. + :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` + :param options: options for the transformation. + :type options: Optional[Dict[str, Any]] + + :raises TransformationError: if the supplied node is not an + intrinsic. + :raises TransformationError: if the supplied node is not an + array-reduction intrinsic. + :raises TransformationError: if there is a dimension argument. + :raises TransformationError: if the array argument is not an array. + :raises TransformationError: if the shape of the array is not + supported. + :raises TransformationError: if the array datatype is not + supported. + :raises TransformationError: if the intrinsic is not part of + an assignment. + + ''' + if not isinstance(node, IntrinsicCall): + raise TransformationError( + f"Error in {self.name} transformation. The supplied node " + f"argument is not an intrinsic, found " + f"'{type(node).__name__}'.") + + if node.routine.name.upper() != self._INTRINSIC_NAME: + raise TransformationError( + f"Error in {self.name} transformation. The supplied node " + f"argument is not a {self._INTRINSIC_NAME.lower()} " + f"intrinsic, found '{node.routine.name}'.") + + array_ref, dim_ref, _ = self._get_args(node) + + # dim_ref is not yet supported by this transformation. + if dim_ref: + raise TransformationError( + f"The dimension argument to {self._INTRINSIC_NAME} is not " + f"yet supported.") + + # There should be at least one arrayreference or reference to + # an array in the expression + # pylint: disable=unidiomatic-typecheck + for reference in array_ref.walk(Reference): + if (isinstance(reference, ArrayReference) or + type(reference) is Reference and + reference.symbol.is_array): + break + else: + raise TransformationError( + f"Error, no ArrayReference's found in the expression " + f"'{array_ref.debug_string()}'.") + + if not node.ancestor(Assignment): + raise TransformationError( + f"{self.name} only works when the intrinsic is part " + f"of an Assignment.") + + assignment = array_ref.ancestor(Assignment) + for this_node in assignment.lhs.walk(Node): + if this_node == array_ref: + raise TransformationError( + "Error, intrinsics on the lhs of an assignment are not " + "currently supported.") + + if len(array_ref.children) == 0: + for shape in array_ref.symbol.shape: + if not (shape in [ + ArrayType.Extent.DEFERRED, ArrayType.Extent.ATTRIBUTE] + or isinstance(shape, ArrayType.ArrayBounds)): + raise TransformationError( + f"Unexpected shape for array. Expecting one of " + f"Deferred, Attribute or Bounds but found '{shape}'.") + + # pylint: disable=too-many-locals + def apply(self, node, options=None): + '''Apply the array-reduction intrinsic conversion transformation to + the specified node. This node must be one of these intrinsic + operations which is converted to an equivalent loop structure. + + :param node: an array-reduction intrinsic. + :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` + :param options: options for the transformation. + :type options: Optional[Dict[str, Any]] + + ''' + self.validate(node) + + orig_lhs = node.ancestor(Assignment).lhs.copy() + orig_rhs = node.ancestor(Assignment).rhs.copy() + + # Determine whether the assignment is an increment (as we have + # to use a temporary if so) e.g. x = x + MAXVAL(a) and store a + # reference to the appropriate variable in new_lhs for future + # use. + lhs_symbol = orig_lhs.symbol + increment = False + for rhs_reference in orig_rhs.walk(Reference): + if rhs_reference.symbol is lhs_symbol: + increment = True + if increment: + new_lhs_symbol = node.scope.symbol_table.new_symbol( + root_name="tmp_var", symbol_type=DataSymbol, + datatype=lhs_symbol.datatype) + new_lhs = Reference(new_lhs_symbol) + else: + new_lhs = orig_lhs.copy() + + expr, _, mask_ref = self._get_args(node) + + # Step 1: replace all references to arrays within the + # intrinsic expressions and mask argument (if it exists) to + # array ranges. For example, 'maxval(a+b, mask=mod(c,2.0)==1)' + # becomes 'maxval(a(:,:)+b(:,:), mask=mod(c(:,:),2.0)==1)' if + # 'a', 'b' and 'c' are 2 dimensional arrays. + rhs = expr.copy() + _ = UnaryOperation.create(UnaryOperation.Operator.PLUS, rhs) + reference2arrayrange = Reference2ArrayRangeTrans() + # The reference to rhs becomes invalid in the following + # transformation so we keep a copy of the parent here and + # reset rhs to rhs_parent.children[0] after the + # transformation. + rhs_parent = rhs.parent + for reference in rhs.walk(Reference): + try: + reference2arrayrange.apply(reference) + except TransformationError: + pass + # Reset rhs from its parent as the previous transformation + # makes the value of rhs become invalid. We know there is only + # one child so can safely use children[0]. + rhs = rhs_parent.children[0] + if mask_ref: + mask_ref_parent = mask_ref.parent + mask_ref_index = mask_ref.position + for reference in mask_ref.walk(Reference): + try: + reference2arrayrange.apply(reference) + except TransformationError: + pass + mask_ref = mask_ref_parent.children[mask_ref_index] + + # Step 2: Put the intrinsic's extracted expression (stored in + # the 'rhs' variable) on the rhs of an argument with one of + # the arrays within the expression being added to the lhs of + # the argument. For example if: + # x = maxval(a(:,:)+b(:,:)) + # then + # rhs = a(:,:)+b(:,:) + # resulting in the following code being created: + # a(:,:) = a(:,:)+b(:,:) + array_refs = rhs.walk(ArrayReference) + # The lhs of the created expression needs to be an array + # reference from the expression itself because the + # ArrayRange2Loop transformation uses it to obtain the loop + # bounds. + lhs = array_refs[0].copy() + + assignment = Assignment.create(lhs, rhs.detach()) + # Replace existing code so the new code gets access to symbol + # tables etc. + orig_assignment = node.ancestor(Assignment) + orig_assignment.replace_with(assignment) + + # Step 3 call nemoarrayrange2loop_trans to create loop bounds + # and array indexing from the array ranges created in step 2 + # (keeping track of where the new loop nest is created). Also + # extract the mask if it exists. For example: + # a(:,:) = a(:,:)+b(:,:) + # becomes + # do idx2 = LBOUND(a,2), UBOUND(a,2) + # do idx = LBOUND(a,1), UBOUND(a,1) + # a(idx,idx2) = a(idx,idx2) + b(idx,idx2) + # enddo + # enddo + if mask_ref: + # add mask to the rhs of the assignment + assignment_rhs = BinaryOperation.create( + BinaryOperation.Operator.AND, assignment.rhs.copy(), + mask_ref.copy()) + assignment.rhs.replace_with(assignment_rhs) + + assignment_parent = assignment.parent + assignment_position = assignment.position + # Must be placed here to avoid circular imports + # pylint: disable=import-outside-toplevel + from psyclone.domain.nemo.transformations import \ + NemoAllArrayRange2LoopTrans + array_range = NemoAllArrayRange2LoopTrans() + array_range.apply(assignment) + outer_loop = assignment_parent.children[assignment_position] + if mask_ref: + # remove mask from the rhs of the assignment + orig_assignment = assignment_rhs.children[0].copy() + indexed_mask_ref = assignment_rhs.children[1].copy() + assignment_rhs.replace_with(orig_assignment) + + # Step 4 convert the original assignment (now within a loop + # and indexed) to its intrinsic form by replacing the + # assignment with new_lhs=INTRINSIC(orig_lhs,). Also + # add in the mask if one has been specified. For example: + # do idx2 = LBOUND(a,2), UBOUND(a,2) + # do idx = LBOUND(a,1), UBOUND(a,1) + # a(idx,idx2) = a(idx,idx2) + b(idx,idx2) + # enddo + # enddo + # becomes + # do idx2 = LBOUND(a,2), UBOUND(a,2) + # do idx = LBOUND(a,1), UBOUND(a,1) + # if (mod(c(idx,idx2),2.0)==1) then + # x = max(x, a(idx,idx2) + b(idx,idx2)) + # end if + # enddo + # enddo + new_assignment = Assignment.create( + new_lhs.copy(), self._loop_body( + new_lhs.copy(), assignment.rhs.copy())) + if mask_ref: + # Place the indexed mask around the statement. + new_assignment = IfBlock.create( + indexed_mask_ref.copy(), [new_assignment]) + + assignment.replace_with(new_assignment) + + # Step 5 initialise the variable and place it before the newly + # created outer loop (in step 2) and deal with any additional + # arguments on the rhs of the original expression. For + # example, if the original code looks like the following: + # x = value1 + maxval(a+b, mask=mod(c,2.0)==1) * value2 + # and the newly created loop looks like the following: + # do idx2 = LBOUND(a,2), UBOUND(a,2) + # do idx = LBOUND(a,1), UBOUND(a,1) + # if (mod(c(idx,idx2),2.0)==1) then + # x = max(x, a(idx,idx2) + b(idx,idx2)) + # end ifx + # enddo + # enddo + # then the result becomes: + # x = tiny(x) + # do idx2 = LBOUND(a,2), UBOUND(a,2) + # do idx = LBOUND(a,1), UBOUND(a,1) + # if (mod(c(idx,idx2),2.0)==1) then + # x = max(x, a(idx,idx2) + b(idx,idx2)) + # end if + # enddo + # enddo + # x = value1 + x * value2 + lhs = new_lhs.copy() + rhs = self._init_var(lhs.symbol) + assignment = Assignment.create(lhs, rhs) + outer_loop.parent.children.insert(outer_loop.position, assignment) + if not (isinstance(orig_rhs, IntrinsicCall) and + orig_rhs.intrinsic is self._INTRINSIC_TYPE): + # The intrinsic call is not the only thing on the rhs of + # the expression, so we need to deal with the additional + # computation. + rhs = orig_rhs.copy() + for child in rhs.walk(IntrinsicCall): + if child.intrinsic is self._INTRINSIC_TYPE: + child.replace_with(new_lhs.copy()) + break + assignment = Assignment.create(orig_lhs.copy(), rhs) + outer_loop.parent.children.insert( + outer_loop.position+1, assignment) + + @abstractmethod + def _loop_body(self, lhs, rhs): + '''The intrinsic-specific content of the created loop body.''' + + @abstractmethod + def _init_var(self, var_symbol): + '''The intrinsic-specific initial value for the temporary variable. + + ''' diff --git a/src/psyclone/psyir/transformations/intrinsics/maxval2code_trans.py b/src/psyclone/psyir/transformations/intrinsics/maxval2loop_trans.py similarity index 54% rename from src/psyclone/psyir/transformations/intrinsics/maxval2code_trans.py rename to src/psyclone/psyir/transformations/intrinsics/maxval2loop_trans.py index f22b3dd063..217ade49a1 100644 --- a/src/psyclone/psyir/transformations/intrinsics/maxval2code_trans.py +++ b/src/psyclone/psyir/transformations/intrinsics/maxval2loop_trans.py @@ -34,22 +34,21 @@ # Author: R. W. Ford, STFC Daresbury Lab '''Module providing a transformation from a PSyIR MAXVAL intrinsic to -PSyIR code. This could be useful if the MAXVAL operator is not -supported by the back-end, the required parallelisation approach, or -if the performance in the inline code is better than the intrinsic. +an equivalent PSyIR loop structure. This could be useful if the MAXVAL +operator is not supported by the back-end, the required +parallelisation approach, or if the performance in the inline code is +better than the intrinsic. ''' -from psyclone.psyir.nodes import ( - Reference, Assignment, IfBlock, BinaryOperation, ArrayReference, - IntrinsicCall) -from psyclone.psyir.transformations.intrinsics.mms_base_trans import ( - MMSBaseTrans) +from psyclone.psyir.nodes import Reference, IntrinsicCall +from psyclone.psyir.transformations.intrinsics.array_reduction_base_trans \ + import ArrayReductionBaseTrans -class Maxval2CodeTrans(MMSBaseTrans): +class Maxval2LoopTrans(ArrayReductionBaseTrans): '''Provides a transformation from a PSyIR MAXVAL IntrinsicCall node to - equivalent code in a PSyIR tree. Validity checks are also - performed. + an equivalent PSyIR loop structure that is suitable for running in + parallel on CPUs and GPUs. Validity checks are also performed. If MAXVAL contains a single positional argument which is an array, the maximum value of all of the elements in the array is returned @@ -67,30 +66,7 @@ class Maxval2CodeTrans(MMSBaseTrans): R = TINY(R) DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) - IF (R < ARRAY(I,J)) THEN - R = ARRAY(I,J) - - If the dimension argument is provided then the maximum value is - returned along the row for each entry in that dimension: - - .. code-block:: fortran - - R = MAXVAL(ARRAY, dimension=2) - - If the array is two dimensional, the equivalent code - for real data is: - - .. code-block:: fortran - - R(:) = TINY(R) - DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) - DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) - IF (R(I) < ARRAY(I,J)) THEN - R(I) = ARRAY(I,J) - - A restriction is that the value of dimension must be able to be - determined by PSyclone, either being a literal or a reference to - something with a known value. + R = MAX(R, ARRAY(I,J)) If the mask argument is provided then the mask is used to determine whether the maxval is applied: @@ -108,26 +84,31 @@ class Maxval2CodeTrans(MMSBaseTrans): DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) IF (MOD(ARRAY(I,J), 2.0)==1) THEN - IF (R < ARRAY(I,J)) THEN - R = ARRAY(I,J) + R = MAX(R, ARRAY(I,J)) - The array passed to MAXVAL may use array syntax, array notation or - array sections (or a mixture of the two), but scalar bounds are - not allowed: + The dimension argument is currently not supported and will result + in a TransformationError exception being raised. + + .. code-block:: fortran + + R = MAXVAL(ARRAY, dimension=2) + + The array passed to MAXVAL may use any combination of array + syntax, array notation, array sections and scalar bounds: .. code-block:: fortran R = MAXVAL(ARRAY) ! array syntax R = MAXVAL(ARRAY(:,:)) ! array notation R = MAXVAL(ARRAY(1:10,lo:hi)) ! array sections - R = MAXVAL(ARRAY(1:10,:)) ! mix of array sections and array notation - R = MAXVAL(ARRAY(1:10,2)) ! NOT SUPPORTED as 2 is a scalar bound + R = MAXVAL(ARRAY(1:10,:)) ! mix of array section and array notation + R = MAXVAL(ARRAY(1:10,2)) ! mix of array section and scalar bound - For example: + An example use of this transformation is given below: >>> from psyclone.psyir.backend.fortran import FortranWriter >>> from psyclone.psyir.frontend.fortran import FortranReader - >>> from psyclone.psyir.transformations import Maxval2CodeTrans + >>> from psyclone.psyir.transformations import Maxval2LoopTrans >>> code = ("subroutine maxval_test(array)\\n" ... " real :: array(10,10)\\n" ... " real :: result\\n" @@ -135,74 +116,43 @@ class Maxval2CodeTrans(MMSBaseTrans): ... "end subroutine\\n") >>> psyir = FortranReader().psyir_from_source(code) >>> sum_node = psyir.children[0].children[0].children[1] - >>> Maxval2CodeTrans().apply(sum_node) + >>> Maxval2LoopTrans().apply(sum_node) >>> print(FortranWriter()(psyir)) subroutine maxval_test(array) real, dimension(10,10) :: array real :: result - real :: maxval_var - integer :: i_0 - integer :: i_1 + integer :: idx + integer :: idx_1 - maxval_var = TINY(maxval_var) - do i_1 = 1, 10, 1 - do i_0 = 1, 10, 1 - if (maxval_var < array(i_0,i_1)) then - maxval_var = array(i_0,i_1) - end if + result = TINY(result) + do idx = 1, 10, 1 + do idx_1 = 1, 10, 1 + result = MAX(result, array(idx_1,idx)) enddo enddo - result = maxval_var end subroutine maxval_test ''' _INTRINSIC_NAME = "MAXVAL" + _INTRINSIC_TYPE = IntrinsicCall.Intrinsic.MAXVAL - def _loop_body(self, array_reduction, array_iterators, var_symbol, - array_ref): + def _loop_body(self, lhs, rhs): '''Provide the body of the nested loop that computes the maximum value - of an array. + of the lhs and rhs. - :param bool array_reduction: True if the implementation should - provide a maximum over a particular array dimension and False - if the maximum is for all elements of the array. - :param array_iterators: a list of datasymbols containing the - loop iterators ordered from innermost loop symbol to outermost - loop symbol. - :type array_iterators: - List[:py:class:`psyclone.psyir.symbols.DataSymbol`] - :param var_symbol: the symbol used to store the final result. - :type var_symbol: :py:class:`psyclone.psyir.symbols.DataSymbol` - :param array_ref: a reference to the array for which the - maximum is being determined. - :type array_ref: :py:class:`psyclone.psyir.nodes.ArrayReference` + :param lhs: the lhs value for the max operation. + :type lhs: :py:class:`psyclone.psyir.nodes.Node` + :param rhs: the rhs value for the max operation. + :type rhs: :py:class:`psyclone.psyir.nodes.Node` - :returns: PSyIR for the body of the nested loop. - :rtype: :py:class:`psyclone.psyir.nodes.IfBlock` + :returns: a MAX IntrinsicCall. + :rtype: :py:class:`psyclone.psyir.nodes.IntrinsicCall` ''' - # maxval_var() = array(i...) - if array_reduction: - array_indices = [Reference(iterator) - for iterator in array_iterators] - lhs = ArrayReference.create(var_symbol, array_indices) - else: - lhs = Reference(var_symbol) - rhs = array_ref - assignment = Assignment.create(lhs, rhs) - - # maxval_var() < array(i...) - lhs = lhs.copy() - rhs = rhs.copy() - if_condition = BinaryOperation.create( - BinaryOperation.Operator.LT, lhs, rhs) - - # if maxval_var() < array(i...) then - # maxval_var() = array(i...) - # end if - return IfBlock.create(if_condition, [assignment]) + # return max(lhs,rhs) + return IntrinsicCall.create(IntrinsicCall.Intrinsic.MAX, [lhs, rhs]) def _init_var(self, var_symbol): '''The initial value for the variable that computes the maximum value diff --git a/src/psyclone/psyir/transformations/intrinsics/minval2code_trans.py b/src/psyclone/psyir/transformations/intrinsics/minval2loop_trans.py similarity index 55% rename from src/psyclone/psyir/transformations/intrinsics/minval2code_trans.py rename to src/psyclone/psyir/transformations/intrinsics/minval2loop_trans.py index 572ee8103d..c31f0ee436 100644 --- a/src/psyclone/psyir/transformations/intrinsics/minval2code_trans.py +++ b/src/psyclone/psyir/transformations/intrinsics/minval2loop_trans.py @@ -34,22 +34,21 @@ # Author: R. W. Ford, STFC Daresbury Lab '''Module providing a transformation from a PSyIR MINVAL intrinsic to -PSyIR code. This could be useful if the MINVAL operator is not -supported by the back-end, the required parallelisation approach, or -if the performance in the inline code is better than the intrinsic. +an equivalent PSyIR loop structure. This could be useful if the MINVAL +operator is not supported by the back-end, the required +parallelisation approach, or if the performance in the inline code is +better than the intrinsic. ''' -from psyclone.psyir.nodes import ( - Reference, Assignment, IfBlock, BinaryOperation, ArrayReference, - IntrinsicCall) -from psyclone.psyir.transformations.intrinsics.mms_base_trans import ( - MMSBaseTrans) +from psyclone.psyir.nodes import Reference, IntrinsicCall +from psyclone.psyir.transformations.intrinsics.array_reduction_base_trans \ + import ArrayReductionBaseTrans -class Minval2CodeTrans(MMSBaseTrans): +class Minval2LoopTrans(ArrayReductionBaseTrans): '''Provides a transformation from a PSyIR MINVAL IntrinsicCall node to - equivalent code in a PSyIR tree. Validity checks are also - performed. + an equivalent PSyIR loop structure that is suitable for running in + parallel on CPUs and GPUs. Validity checks are also performed. If MINVAL contains a single positional argument which is an array, the minimum value of all of the elements in the array is returned @@ -67,30 +66,7 @@ class Minval2CodeTrans(MMSBaseTrans): R = HUGE(R) DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) - IF (R > ARRAY(I,J)) THEN - R = ARRAY(I,J) - - If the dimension argument is provided then the minimum value is - returned along the row for each entry in that dimension: - - .. code-block:: fortran - - R = MINVAL(ARRAY, dimension=2) - - If the array is two dimensional, the equivalent code - for real data is: - - .. code-block:: fortran - - R(:) = HUGE(R) - DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) - DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) - IF (R(I) > ARRAY(I,J)) THEN - R(I) = ARRAY(I,J) - - A restriction is that the value of dimension must be able to be - determined by PSyclone, either being a literal or a reference to - something with a known value. + R = MIN(R, ARRAY(I,J)) If the mask argument is provided then the mask is used to determine whether the minval is applied: @@ -108,26 +84,31 @@ class Minval2CodeTrans(MMSBaseTrans): DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) IF (MOD(ARRAY(I,J), 2.0)==1) THEN - IF (R > ARRAY(I,J)) THEN - R = ARRAY(I,J) + R = MIN(R, ARRAY(I,J)) - The array passed to MINVAL may use array syntax, array notation or - array sections (or a mixture of the two), but scalar bounds are - not allowed: + The dimension argument is currently not supported and will result + in a TransformationError exception being raised. + + .. code-block:: fortran + + R = MINVAL(ARRAY, dimension=2) + + The array passed to MINVAL may use any combination of array + syntax, array notation, array sections and scalar bounds: .. code-block:: fortran R = MINVAL(ARRAY) ! array syntax R = MINVAL(ARRAY(:,:)) ! array notation R = MINVAL(ARRAY(1:10,lo:hi)) ! array sections - R = MINVAL(ARRAY(1:10,:)) ! mix of array sections and array notation - R = MINVAL(ARRAY(1:10,2)) ! NOT SUPPORTED as 2 is a scalar bound + R = MINVAL(ARRAY(1:10,:)) ! mix of array section and array notation + R = MINVAL(ARRAY(1:10,2)) ! mix of array section and scalar bound For example: >>> from psyclone.psyir.backend.fortran import FortranWriter >>> from psyclone.psyir.frontend.fortran import FortranReader - >>> from psyclone.psyir.transformations import Minval2CodeTrans + >>> from psyclone.psyir.transformations import Minval2LoopTrans >>> code = ("subroutine minval_test(array)\\n" ... " real :: array(10,10)\\n" ... " real :: result\\n" @@ -135,74 +116,43 @@ class Minval2CodeTrans(MMSBaseTrans): ... "end subroutine\\n") >>> psyir = FortranReader().psyir_from_source(code) >>> sum_node = psyir.children[0].children[0].children[1] - >>> Minval2CodeTrans().apply(sum_node) + >>> Minval2LoopTrans().apply(sum_node) >>> print(FortranWriter()(psyir)) subroutine minval_test(array) real, dimension(10,10) :: array real :: result - real :: minval_var - integer :: i_0 - integer :: i_1 + integer :: idx + integer :: idx_1 - minval_var = HUGE(minval_var) - do i_1 = 1, 10, 1 - do i_0 = 1, 10, 1 - if (minval_var > array(i_0,i_1)) then - minval_var = array(i_0,i_1) - end if + result = HUGE(result) + do idx = 1, 10, 1 + do idx_1 = 1, 10, 1 + result = MIN(result, array(idx_1,idx)) enddo enddo - result = minval_var end subroutine minval_test ''' _INTRINSIC_NAME = "MINVAL" + _INTRINSIC_TYPE = IntrinsicCall.Intrinsic.MINVAL - def _loop_body(self, array_reduction, array_iterators, var_symbol, - array_ref): + def _loop_body(self, lhs, rhs): '''Provide the body of the nested loop that computes the minimum value - of an array. + of the lhs and rhs. - :param bool array_reduction: True if the implementation should - provide a minimum over a particular array dimension and False - if the minimum is for all elements of the array. - :param array_iterators: a list of datasymbols containing the - loop iterators ordered from outermost loop symbol to innermost - loop symbol. - :type array_iterators: - List[:py:class:`psyclone.psyir.symbols.DataSymbol`] - :param var_symbol: the symbol used to store the final result. - :type var_symbol: :py:class:`psyclone.psyir.symbols.DataSymbol` - :param array_ref: a reference to the array for which the - minimum is being determined. - :type array_ref: :py:class:`psyclone.psyir.nodes.ArrayReference` + :param lhs: the lhs value for the min operation. + :type lhs: :py:class:`psyclone.psyir.nodes.Node` + :param rhs: the rhs value for the min operation. + :type rhs: :py:class:`psyclone.psyir.nodes.Node` - :returns: PSyIR for the body of the nested loop. - :rtype: :py:class:`psyclone.psyir.nodes.IfBlock` + :returns: a MIN IntrinsicCall. + :rtype: :py:class:`psyclone.psyir.nodes.IntrinsicCall` ''' - # minval_var() = array(i...) - if array_reduction: - array_indices = [Reference(iterator) - for iterator in array_iterators] - lhs = ArrayReference.create(var_symbol, array_indices) - else: - lhs = Reference(var_symbol) - rhs = array_ref - assignment = Assignment.create(lhs, rhs) - - # minval_var() > array(i...) - lhs = lhs.copy() - rhs = rhs.copy() - if_condition = BinaryOperation.create( - BinaryOperation.Operator.GT, lhs, rhs) - - # if minval_var() > array(i...) then - # minval_var() = array(i...) - # end if - return IfBlock.create(if_condition, [assignment]) + # return min(lhs,rhs) + return IntrinsicCall.create(IntrinsicCall.Intrinsic.MIN, [lhs, rhs]) def _init_var(self, var_symbol): '''The initial value for the variable that computes the minimum value diff --git a/src/psyclone/psyir/transformations/intrinsics/mms_base_trans.py b/src/psyclone/psyir/transformations/intrinsics/mms_base_trans.py deleted file mode 100644 index cc74a57318..0000000000 --- a/src/psyclone/psyir/transformations/intrinsics/mms_base_trans.py +++ /dev/null @@ -1,376 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2023, 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. -# ----------------------------------------------------------------------------- -# Author: R. W. Ford, STFC Daresbury Lab -# Modified: S. Siso, STFC Daresbury Lab - -'''Module providing common functionality to transformation from a -PSyIR SUM, MINVAL or MAXVAL intrinsic to PSyIR code. - -''' -from abc import ABC, abstractmethod - -from psyclone.psyir.nodes import ( - Assignment, Reference, Literal, Loop, ArrayReference, IfBlock, Range, - IntrinsicCall) -from psyclone.psyir.symbols import ( - DataSymbol, INTEGER_TYPE, ScalarType, ArrayType) -from psyclone.psyGen import Transformation -from psyclone.psyir.transformations.transformation_error import \ - TransformationError - - -class MMSBaseTrans(Transformation, ABC): - '''An abstract parent class providing common functionality to the - sum2code_trans, minval2code_trans and maxval2_code trans - transformations. - - ''' - _INTRINSIC_NAME = None - - @staticmethod - def _get_args(node): - '''Utility method that returns the minval, maxval or sum arguments, - (array reference, dimension and mask). - - :param node: a minval, maxval or sum intrinsic. - :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` - - returns: a tuple containing the 3 arguments. - rtype: Tuple[py:class:`psyclone.psyir.nodes.reference.Reference`, - py:class:`psyclone.psyir.nodes.Literal` | - :py:class:`psyclone.psyir.nodes.Reference`, - Optional[:py:class:`psyclone.psyir.nodes.Node`]] - - ''' - # Determine the arguments to the intrinsic - args = [None, None, None] - arg_names_map = {"array": 0, "dim": 1, "mask": 2} - for idx, child in enumerate(node.children): - if not node.argument_names[idx]: - # positional arg - args[idx] = child - else: - # named arg - name = node.argument_names[idx].lower() - args[arg_names_map[name]] = child - return tuple(args) - - def __str__(self): - return (f"Convert the PSyIR {self._INTRINSIC_NAME} intrinsic " - "to equivalent PSyIR code.") - - # pylint: disable=too-many-branches - def validate(self, node, options=None): - '''Check that the input node is valid before applying the - transformation. - - :param node: a Sum, Minval or Maxval intrinsic. - :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` - :param options: options for the transformation. - :type options: Optional[Dict[str, Any]] - - :raises TransformationError: if the supplied node is not an - intrinsic. - :raises TransformationError: if the supplied node is not a sum, - minval, or maxval intrinsic. - :raises TransformationError: if a valid value for the - dimension argument can't be determined. - :raises TransformationError: if the array argument is not an array. - :raises TransformationError: if the shape of the array is not - supported. - :raises TransformationError: if the array datatype is not - supported. - :raises TransformationError: if the intrinsic is not part of - an assignment. - - ''' - if not isinstance(node, IntrinsicCall): - raise TransformationError( - f"Error in {self.name} transformation. The supplied node " - f"argument is not an intrinsic, found " - f"'{type(node).__name__}'.") - - if node.routine.name.upper() != self._INTRINSIC_NAME: - raise TransformationError( - f"Error in {self.name} transformation. The supplied node " - f"argument is not a {self._INTRINSIC_NAME.lower()} " - f"intrinsic, found '{node.routine.name}'.") - - array_ref, dim_ref, _ = self._get_args(node) - - # If there is a dim argument then PSyclone curently needs to - # be able to determine the literal value. - # pylint: disable=unidiomatic-typecheck - if dim_ref and not ( - isinstance(dim_ref, Literal) - or (type(dim_ref) is Reference and - dim_ref.symbol.is_constant and - isinstance(dim_ref.symbol.initial_value, Literal))): - if isinstance(dim_ref, Reference): - info = f"a reference to a '{type(dim_ref.symbol).__name__}'" - else: - info = f"type '{type(dim_ref).__name__}'" - raise TransformationError( - f"Can't find the value of the 'dim' argument to the " - f"{self._INTRINSIC_NAME} intrinsic. Expected " - f"it to be a literal or a reference to a known constant " - f"value, but found '{dim_ref.debug_string()}' which is " - f"{info}.") - - # pylint: disable=unidiomatic-typecheck - if not (isinstance(array_ref, ArrayReference) or - (type(array_ref) is Reference)): - raise TransformationError( - f"{self.name} only supports arrays or plain references for " - f"the first argument, but found '{type(array_ref).__name__}'.") - - if len(array_ref.children) == 0: - if not array_ref.symbol.is_array: - raise TransformationError( - f"Expected '{array_ref.name}' to be an array.") - for shape in array_ref.symbol.shape: - if not (shape in [ - ArrayType.Extent.DEFERRED, ArrayType.Extent.ATTRIBUTE] - or isinstance(shape, ArrayType.ArrayBounds)): - raise TransformationError( - f"Unexpected shape for array. Expecting one of " - f"Deferred, Attribute or Bounds but found '{shape}'.") - - for shape in array_ref.children: - if not isinstance(shape, Range): - raise TransformationError( - f"{self.name} only supports arrays with array ranges, " - f"but found a fixed dimension in " - f"'{array_ref.debug_string()}'.") - - array_intrinsic = array_ref.symbol.datatype.intrinsic - if array_intrinsic not in [ScalarType.Intrinsic.REAL, - ScalarType.Intrinsic.INTEGER]: - raise TransformationError( - f"Only real and integer types supported for array " - f"'{array_ref.name}', but found '{array_intrinsic.name}'.") - - if not node.ancestor(Assignment): - raise TransformationError( - f"{self.name} only works when the intrinsic is part " - f"of an Assignment.") - - # pylint: disable=too-many-locals - # pylint: disable=too-many-branches - # pylint: disable=too-many-statements - def apply(self, node, options=None): - '''Apply the SUM, MINVAL or MAXVAL intrinsic conversion transformation - to the specified node. This node must be one of these - intrinsic operations which is converted to equivalent inline - code. - - :param node: a Sum, Minval or Maxval intrinsic. - :type node: :py:class:`psyclone.psyir.nodes.IntrinsicCall` - :param options: options for the transformation. - :type options: Optional[Dict[str, Any]] - - ''' - self.validate(node) - - array_ref, dimension_ref, mask_ref = self._get_args(node) - - # Determine the literal value of the dimension argument - dimension_literal = None - # pylint: disable=unidiomatic-typecheck - if not dimension_ref: - # there is no dimension argument - pass - elif isinstance(dimension_ref, Literal): - dimension_literal = dimension_ref - elif ((type(dimension_ref) is Reference) and - dimension_ref.symbol.is_constant): - dimension_literal = dimension_ref.symbol.initial_value - # else exception is handled by the validate method. - - # Determine the dimension and extent of the array - ndims = None - allocatable = False - if len(array_ref.children) == 0: - # There is no bounds information in the array reference, - # so look at the declaration. - # Note, the potential 'if not array_ref.symbol.is_array:' - # exception is already handled by the validate method. - ndims = len(array_ref.datatype.shape) - - loop_bounds = [] - for idx, shape in enumerate(array_ref.datatype.shape): - if shape in [ArrayType.Extent.DEFERRED, - ArrayType.Extent.ATTRIBUTE]: - if shape == ArrayType.Extent.DEFERRED: - allocatable = True - # runtime extent using LBOUND and UBOUND required - lbound = IntrinsicCall.create( - IntrinsicCall.Intrinsic.LBOUND, - [Reference(array_ref.symbol), - ("dim", Literal(str(idx+1), INTEGER_TYPE))]) - ubound = IntrinsicCall.create( - IntrinsicCall.Intrinsic.UBOUND, - [Reference(array_ref.symbol), - ("dim", Literal(str(idx+1), INTEGER_TYPE))]) - loop_bounds.append((lbound, ubound)) - elif isinstance(shape, ArrayType.ArrayBounds): - # array extent is defined in the array declaration - loop_bounds.append(shape) - # Note, the validate method guarantees that an else - # clause is not required. - else: - # The validate method guarantees that this is an array - # reference. - loop_bounds = [] - ndims = len(array_ref.children) - for shape in array_ref.children: - loop_bounds.append((shape.start.copy(), shape.stop.copy())) - - # Determine the datatype of the array's values and create a - # scalar of that type - array_intrinsic = array_ref.datatype.intrinsic - array_precision = array_ref.datatype.precision - scalar_type = ScalarType(array_intrinsic, array_precision) - - symbol_table = node.scope.symbol_table - assignment = node.ancestor(Assignment) - - datatype = scalar_type - array_reduction = False - if dimension_ref and ndims > 1: - array_reduction = True - # We are reducing from one array to another - shape = [] - for idx, bounds in enumerate(loop_bounds): - # The validation constrains the transformation to only - # allow cases where the literal value for dimension - # is known. - if int(dimension_literal.value)-1 == idx: - # This is the dimension we are performing the - # reduction over so do not loop over it. - pass - else: - shape.append(bounds) - if allocatable: - # Reduction and allocatable means we need to make the - # reduction array allocatable. We keep the bounds in - # datatype_keep for allocating the array. - datatype = ArrayType( - scalar_type, len(shape)*[ArrayType.Extent.DEFERRED]) - else: - datatype = ArrayType(scalar_type, shape) - - # Detach the intrinsics array-reference argument to the - # intrinsic as it will be used later within a loop nest. - array_ref = node.children[0].detach() - - # Create temporary variable based on the name of the intrinsic. - var_symbol = symbol_table.new_symbol( - f"{self._INTRINSIC_NAME.lower()}_var", symbol_type=DataSymbol, - datatype=datatype) - # Replace operation with a temporary variable. - if array_reduction: - # This is a reduction so the number of array dimensions is - # reduced by 1 cf. the original array. - array_indices = (ndims - 1)*[":"] - reference = ArrayReference.create(var_symbol, array_indices) - else: - reference = Reference(var_symbol) - node.replace_with(reference) - - if allocatable and array_reduction: - range_list = [ - Range.create(lbound, ubound) for (lbound, ubound) in shape] - # Allocate the reduction and place it just before it is - # initialised. - allocate = IntrinsicCall.create( - IntrinsicCall.Intrinsic.ALLOCATE, - [ArrayReference.create(var_symbol, range_list)]) - assignment = reference.parent - assignment.parent.children.insert(assignment.position, allocate) - - # Create the loop iterators - loop_iterators = [] - array_iterators = [] - for idx in range(ndims): - loop_iterator = symbol_table.new_symbol( - f"i_{idx}", symbol_type=DataSymbol, datatype=INTEGER_TYPE) - loop_iterators.append(loop_iterator) - if array_reduction and idx != int(dimension_literal.value)-1: - array_iterators.append(loop_iterator) - - # Initialise the temporary variable. - rhs = self._init_var(var_symbol) - lhs = reference.copy() - new_assignment = Assignment.create(lhs, rhs) - assignment.parent.children.insert(assignment.position, new_assignment) - - array_indices = [] - for idx in range(ndims): - array_indices.append(Reference(loop_iterators[idx])) - array_ref = ArrayReference.create(array_ref.symbol, array_indices) - - statement = self._loop_body( - array_reduction, array_iterators, var_symbol, array_ref) - - if mask_ref: - # A mask argument has been provided - for ref in mask_ref.walk(Reference): - # pylint: disable=unidiomatic-typecheck - if ref.name == array_ref.name and type(ref) is Reference: - # The array is not indexed so it needs indexing - # for the loop nest. - shape = [Reference(obj) for obj in loop_iterators] - reference = ArrayReference.create(ref.symbol, shape) - ref.replace_with(reference) - statement = IfBlock.create(mask_ref.detach(), [statement]) - - for idx in range(ndims): - statement = Loop.create( - loop_iterators[idx].copy(), loop_bounds[idx][0].copy(), - loop_bounds[idx][1].copy(), Literal("1", INTEGER_TYPE), - [statement]) - - assignment.parent.children.insert(assignment.position, statement) - - @abstractmethod - def _loop_body( - self, array_reduction, array_iterators, var_symbol, array_ref): - '''The intrinsic-specific content of the created loop body.''' - - @abstractmethod - def _init_var(self, var_symbol): - '''The intrinsic-specific initial value for the temporary variable. - - ''' diff --git a/src/psyclone/psyir/transformations/intrinsics/product2loop_trans.py b/src/psyclone/psyir/transformations/intrinsics/product2loop_trans.py new file mode 100644 index 0000000000..01732e6ab4 --- /dev/null +++ b/src/psyclone/psyir/transformations/intrinsics/product2loop_trans.py @@ -0,0 +1,179 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023, 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. +# ----------------------------------------------------------------------------- +# Author: R. W. Ford, STFC Daresbury Lab + +'''Module providing a transformation from a PSyIR PRODUCT intrinsic to +an equivalent PSyIR loop structure. This could be useful if the PRODUCT +operator is not supported by the back-end, the required +parallelisation approach, or if the performance in the inline code is +better than the intrinsic. + +''' +from psyclone.psyir.nodes import IntrinsicCall, BinaryOperation, Literal +from psyclone.psyir.symbols import ScalarType +from psyclone.psyir.transformations.intrinsics.array_reduction_base_trans \ + import ArrayReductionBaseTrans + + +class Product2LoopTrans(ArrayReductionBaseTrans): + '''Provides a transformation from a PSyIR PRODUCT IntrinsicCall node to + an equivalent PSyIR loop structure that is suitable for running in + parallel on CPUs and GPUs. Validity checks are also performed. + + If PRODUCT contains a single positional argument which is an array, + the maximum value of all of the elements in the array is returned + in the the scalar R. + + .. code-block:: fortran + + R = PRODUCT(ARRAY) + + For example, if the array is two dimensional, the equivalent code + for real data is: + + .. code-block:: fortran + + R = 1.0 + DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) + DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) + R = R * ARRAY(I,J) + + If the mask argument is provided then the mask is used to + determine whether the product is applied: + + .. code-block:: fortran + + R = PRODUCT(ARRAY, mask=MOD(ARRAY, 2.0)==1) + + If the array is two dimensional, the equivalent code + for real data is: + + .. code-block:: fortran + + R = 1.0 + DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) + DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) + IF (MOD(ARRAY(I,J), 2.0)==1) THEN + R = R * ARRAY(I,J) + + The dimension argument is currently not supported and will result + in a TransformationError exception being raised. + + .. code-block:: fortran + + R = PRODUCT(ARRAY, dimension=2) + + The array passed to PRODUCT may use any combination of array + syntax, array notation, array sections and scalar bounds: + + .. code-block:: fortran + + R = PRODUCT(ARRAY) ! array syntax + R = PRODUCT(ARRAY(:,:)) ! array notation + R = PRODUCT(ARRAY(1:10,lo:hi)) ! array sections + R = PRODUCT(ARRAY(1:10,:)) ! mix of array section and array notation + R = PRODUCT(ARRAY(1:10,2)) ! mix of array section and scalar bound + + An example use of this transformation is given below: + + >>> from psyclone.psyir.backend.fortran import FortranWriter + >>> from psyclone.psyir.frontend.fortran import FortranReader + >>> from psyclone.psyir.transformations import Product2LoopTrans + >>> code = ("subroutine product_test(array)\\n" + ... " real :: array(10,10)\\n" + ... " real :: result\\n" + ... " result = product(array)\\n" + ... "end subroutine\\n") + >>> psyir = FortranReader().psyir_from_source(code) + >>> product_node = psyir.children[0].children[0].children[1] + >>> Product2LoopTrans().apply(product_node) + >>> print(FortranWriter()(psyir)) + subroutine product_test(array) + real, dimension(10,10) :: array + real :: result + integer :: idx + integer :: idx_1 + + result = 1.0 + do idx = 1, 10, 1 + do idx_1 = 1, 10, 1 + result = result * array(idx_1,idx) + enddo + enddo + + end subroutine product_test + + + ''' + _INTRINSIC_NAME = "PRODUCT" + _INTRINSIC_TYPE = IntrinsicCall.Intrinsic.PRODUCT + + def _loop_body(self, lhs, rhs): + '''Provide the body of the nested loop that computes the maximum value + of the lhs and rhs. + + :param lhs: the lhs value for the product operation. + :type lhs: :py:class:`psyclone.psyir.nodes.Node` + :param rhs: the rhs value for the product operation. + :type rhs: :py:class:`psyclone.psyir.nodes.Node` + + :returns: the product of the lhs and rhs. + :rtype: :py:class:`psyclone.psyir.nodes.BinaryOperation` + + ''' + # return lhs * rhs + return BinaryOperation.create(BinaryOperation.Operator.MUL, lhs, rhs) + + def _init_var(self, var_symbol): + '''The initial value for the variable that computes the product + of an array. + + :param var_symbol: the symbol used to store the final result. + :type var_symbol: :py:class:`psyclone.psyir.symbols.DataSymbol` + + :returns: PSyIR for the value to initialise the variable that + computes the product. + :rtype: :py:class:`psyclone.psyir.nodes.IntrinsicCall` + + ''' + intrinsic = var_symbol.datatype.intrinsic + precision = var_symbol.datatype.precision + scalar_type = ScalarType(intrinsic, precision) + if intrinsic == ScalarType.Intrinsic.REAL: + value_str = "1.0" + elif intrinsic == ScalarType.Intrinsic.INTEGER: + value_str = "1" + # Note, the validate method guarantees that an else branch is + # not required. + return Literal(value_str, scalar_type) diff --git a/src/psyclone/psyir/transformations/intrinsics/sum2code_trans.py b/src/psyclone/psyir/transformations/intrinsics/sum2loop_trans.py similarity index 59% rename from src/psyclone/psyir/transformations/intrinsics/sum2code_trans.py rename to src/psyclone/psyir/transformations/intrinsics/sum2loop_trans.py index 527dffba8b..cdac6a449e 100644 --- a/src/psyclone/psyir/transformations/intrinsics/sum2code_trans.py +++ b/src/psyclone/psyir/transformations/intrinsics/sum2loop_trans.py @@ -34,23 +34,23 @@ # Author: R. W. Ford, STFC Daresbury Lab # Modified: S. Siso, STFC Daresbury Lab -'''Module providing a transformation from a PSyIR SUM intrinsic to -PSyIR code. This could be useful if the SUM intrinsic is not supported -by the back-end, the required parallelisation approach, or if the -performance in the inline code is better than the intrinsic. +'''Module providing a transformation from a PSyIR SUM intrinsic to an +equivalent PSyIR loop structure. This could be useful if the SUM +intrinsic is not supported by the back-end, the required +parallelisation approach, or if the performance in the inline code is +better than the intrinsic. ''' -from psyclone.psyir.nodes import ( - BinaryOperation, Assignment, Reference, Literal, ArrayReference) +from psyclone.psyir.nodes import BinaryOperation, Literal, IntrinsicCall from psyclone.psyir.symbols import ScalarType -from psyclone.psyir.transformations.intrinsics.mms_base_trans import ( - MMSBaseTrans) +from psyclone.psyir.transformations.intrinsics.array_reduction_base_trans \ + import ArrayReductionBaseTrans -class Sum2CodeTrans(MMSBaseTrans): - '''Provides a transformation from a PSyIR SUM IntrinsicCall node to - equivalent code in a PSyIR tree. Validity checks are also - performed. +class Sum2LoopTrans(ArrayReductionBaseTrans): + '''Provides a transformation from a PSyIR SUM IntrinsicCall node to an + equivalent PSyIR loop structure that is suitable for running in + parallel on CPUs and GPUs. Validity checks are also performed. If SUM contains a single positional argument which is an array, all elements of that array are summed and the result returned in @@ -70,27 +70,6 @@ class Sum2CodeTrans(MMSBaseTrans): DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) R = R + ARRAY(I,J) - If the dimension argument is provided then only that dimension is - summed: - - .. code-block:: fortran - - R = SUM(ARRAY, dimension=2) - - If the array is two dimensional, the equivalent code - for real data is: - - .. code-block:: fortran - - R(:) = 0.0 - DO J=LBOUND(ARRAY,2),UBOUND(ARRAY,2) - DO I=LBOUND(ARRAY,1),UBOUND(ARRAY,1) - R(I) = R(I) + ARRAY(I,J) - - A restriction is that the value of dimension must be able to be - determined by PSyclone, either being a literal or a reference to - something with a known value. - If the mask argument is provided then the mask is used to determine whether the sum is applied: @@ -109,23 +88,29 @@ class Sum2CodeTrans(MMSBaseTrans): IF (MOD(ARRAY(I,J), 2.0)==1) THEN R = R + ARRAY(I,J) - The array passed to SUM may use array syntax, array notation or - array sections (or a mixture of the two), but scalar bounds are - not allowed: + The dimension argument is currently not supported and will result + in a TransformationError exception being raised. + + .. code-block:: fortran + + R = SUM(ARRAY, dimension=2) + + The array passed to MAXVAL may use any combination of array + syntax, array notation, array sections and scalar bounds: .. code-block:: fortran R = SUM(ARRAY) ! array syntax R = SUM(ARRAY(:,:)) ! array notation R = SUM(ARRAY(1:10,lo:hi)) ! array sections - R = SUM(ARRAY(1:10,:)) ! mix of array sections and array notation - R = SUM(ARRAY(1:10,2)) ! NOT SUPPORTED as 2 is a scalar bound + R = SUM(ARRAY(1:10,:)) ! mix of array section and array notation + R = SUM(ARRAY(1:10,2)) ! mix of array section and scalar bound For example: >>> from psyclone.psyir.backend.fortran import FortranWriter >>> from psyclone.psyir.frontend.fortran import FortranReader - >>> from psyclone.psyir.transformations import Sum2CodeTrans + >>> from psyclone.psyir.transformations import Sum2LoopTrans >>> code = ("subroutine sum_test(array,n,m)\\n" ... " integer :: n, m\\n" ... " real :: array(10,10)\\n" @@ -134,71 +119,45 @@ class Sum2CodeTrans(MMSBaseTrans): ... "end subroutine\\n") >>> psyir = FortranReader().psyir_from_source(code) >>> sum_node = psyir.children[0].children[0].children[1] - >>> Sum2CodeTrans().apply(sum_node) + >>> Sum2LoopTrans().apply(sum_node) >>> print(FortranWriter()(psyir)) subroutine sum_test(array, n, m) integer :: n integer :: m real, dimension(10,10) :: array real :: result - real :: sum_var - integer :: i_0 - integer :: i_1 + integer :: idx + integer :: idx_1 - sum_var = 0.0 - do i_1 = 1, 10, 1 - do i_0 = 1, 10, 1 - sum_var = sum_var + array(i_0,i_1) + result = 0.0 + do idx = 1, 10, 1 + do idx_1 = 1, 10, 1 + result = result + array(idx_1,idx) enddo enddo - result = sum_var end subroutine sum_test ''' _INTRINSIC_NAME = "SUM" + _INTRINSIC_TYPE = IntrinsicCall.Intrinsic.SUM - def _loop_body(self, array_reduction, array_iterators, var_symbol, - array_ref): - '''Provide the body of the nested loop that computes the sum of an - array. - - :param bool array_reduction: True if the implementation should - provide a sum over a particular array dimension and False - if the sum is for all elements of the array. - :param array_iterators: a list of datasymbols containing the - loop iterators ordered from outermost loop symbol to innermost - loop symbol. - :type array_iterators: - List[:py:class:`psyclone.psyir.symbols.DataSymbol`] - :param var_symbol: the symbol used to store the final result. - :type var_symbol: :py:class:`psyclone.psyir.symbols.DataSymbol` - :param array_ref: a reference to the array for which the - sum is being determined. - :type array_ref: :py:class:`psyclone.psyir.nodes.ArrayReference` + def _loop_body(self, lhs, rhs): + '''Provide the body of the nested loop that computes the sum + of the lhs and rhs. + + :param lhs: the lhs value for the sum operation. + :type lhs: :py:class:`psyclone.psyir.nodes.Node` + :param rhs: the rhs value for the sum operation. + :type rhs: :py:class:`psyclone.psyir.nodes.Node` - :returns: PSyIR for the body of the nested loop. - :rtype: :py:class:`psyclone.psyir.nodes.Assignment` + :returns: the sum of the lhs and rhs. + :rtype: :py:class:`psyclone.psyir.nodes.BinaryOperation` ''' - if array_reduction: - # sum_var(i,...) = sum_var(i,...) + array(i,...) - array_indices = [Reference(iterator) - for iterator in array_iterators] - lhs = ArrayReference.create(var_symbol, array_indices) - array_indices = [Reference(iterator) - for iterator in array_iterators] - rhs_child1 = ArrayReference.create(var_symbol, array_indices) - else: - # sum_var = sum_var + array(i,...) - lhs = Reference(var_symbol) - rhs_child1 = Reference(var_symbol) - - rhs_child2 = array_ref - rhs = BinaryOperation.create(BinaryOperation.Operator.ADD, rhs_child1, - rhs_child2) - return Assignment.create(lhs, rhs) + # return lhs + rhs + return BinaryOperation.create(BinaryOperation.Operator.ADD, lhs, rhs) def _init_var(self, var_symbol): '''The initial value for the variable that computes the sum diff --git a/src/psyclone/psyir/transformations/reference2arrayrange_trans.py b/src/psyclone/psyir/transformations/reference2arrayrange_trans.py index 372ee46862..4f320e10da 100644 --- a/src/psyclone/psyir/transformations/reference2arrayrange_trans.py +++ b/src/psyclone/psyir/transformations/reference2arrayrange_trans.py @@ -182,6 +182,5 @@ def apply(self, node, options=None): lbound, ubound, step = \ Reference2ArrayRangeTrans._get_array_bound(symbol, idx) indices.append(Range.create(lbound, ubound, step)) - array_ref = ArrayReference.create(symbol, indices) node.replace_with(array_ref) diff --git a/src/psyclone/tests/config_test.py b/src/psyclone/tests/config_test.py index a020b17d59..c6857febd8 100644 --- a/src/psyclone/tests/config_test.py +++ b/src/psyclone/tests/config_test.py @@ -73,6 +73,7 @@ REPROD_PAD_SIZE = 8 VALID_PSY_DATA_PREFIXES = profile, extract OCL_DEVICES_PER_NODE = 1 +BACKEND_CHECKS_ENABLED = false [dynamo0.3] access_mapping = gh_read: read, gh_write: write, gh_readwrite: readwrite, gh_inc: inc, gh_sum: sum @@ -117,15 +118,14 @@ def clear_config_instance(): Config._instance = None -# Disable this pylint warning because otherwise it gets upset about the -# use of these fixtures in the test code. -# pylint:disable=redefined-outer-name -@pytest.fixture(scope="module", +@pytest.fixture(name="bool_entry", + scope="module", params=["DISTRIBUTED_MEMORY", "REPRODUCIBLE_REDUCTIONS", "COMPUTE_ANNEXED_DOFS", - "RUN_TIME_CHECKS"]) -def bool_entry(request): + "RUN_TIME_CHECKS", + "BACKEND_CHECKS_ENABLED"]) +def bool_entry_fixture(request): ''' Parameterised fixture that will cause a test that has it as an argument to be run for each boolean member of the configuration file @@ -138,9 +138,10 @@ def bool_entry(request): return request.param -@pytest.fixture(scope="module", +@pytest.fixture(name="int_entry", + scope="module", params=["REPROD_PAD_SIZE", "OCL_DEVICES_PER_NODE"]) -def int_entry(request): +def int_entry_fixture(request): ''' Parameterised fixture that returns the names of integer members of the configuration file. @@ -152,7 +153,7 @@ def int_entry(request): return request.param -def config(config_file, content): +def get_config(config_file, content): ''' A utility function that creates and populates a temporary PSyclone configuration file for testing purposes. @@ -365,7 +366,7 @@ def test_api_not_in_list(tmpdir): config_file = tmpdir.join("config") with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert ("The API (invalid) is not in the list of " "supported APIs" in str(err.value)) @@ -383,7 +384,7 @@ def test_default_stubapi_invalid(tmpdir): flags=re.MULTILINE) with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert ("The default stub API (invalid) is not in the list of " "supported stub APIs" in str(err.value)) @@ -400,7 +401,7 @@ def test_default_stubapi_missing(tmpdir): _CONFIG_CONTENT, flags=re.MULTILINE) - test_config = config(config_file, content) + test_config = get_config(config_file, content) assert test_config.default_stub_api == test_config.default_api @@ -417,7 +418,7 @@ def test_not_bool(bool_entry, tmpdir): flags=re.MULTILINE) with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert "configuration error (file=" in str(err.value) assert f": Error while parsing {bool_entry}" in str(err.value) @@ -436,13 +437,40 @@ def test_not_int(int_entry, tmpdir): flags=re.MULTILINE) with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert "configuration error (file=" in str(err.value) assert (f": error while parsing {int_entry}: invalid literal" in str(err.value)) +def test_backend_checks_from_file(tmpdir): + ''' + Check that the value for BACKEND_CHECKS_ENABLED is correctly read from + the config. file and defaults to True. + + ''' + config_file = tmpdir.join("config") + cfg = get_config(config_file, _CONFIG_CONTENT) + assert cfg.backend_checks_enabled is False + content = re.sub(r"^BACKEND_CHECKS_ENABLED = false$", + "BACKEND_CHECKS_ENABLED = true", + _CONFIG_CONTENT, + flags=re.MULTILINE) + config_file2 = tmpdir.join("config2") + cfg2 = get_config(config_file2, content) + assert cfg2.backend_checks_enabled is True + # Remove it from the config file. + content = re.sub(r"^BACKEND_CHECKS_ENABLED = false$", + "", + _CONFIG_CONTENT, + flags=re.MULTILINE) + config_file3 = tmpdir.join("config3") + cfg3 = get_config(config_file3, content) + # Defaults to True if not specified in the file. + assert cfg3.backend_checks_enabled is True + + def test_broken_fmt(tmpdir): ''' Check the error if the formatting of the configuration file is wrong. @@ -454,7 +482,7 @@ def test_broken_fmt(tmpdir): content = "COMPUTE_ANNEXED_DOFS = false\n" with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert ("ConfigParser failed to read the configuration file. Is it " "formatted correctly? (Error was: File contains no section " "headers" in str(err.value)) @@ -466,7 +494,7 @@ def test_broken_fmt(tmpdir): flags=re.MULTILINE) with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert "Error was: Source contains parsing errors" in str(err.value) @@ -482,7 +510,7 @@ def test_default_missing(tmpdir): ''' with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) assert "configuration error (file=" in str(err.value) assert "Configuration file has no [DEFAULT] section" in str(err.value) @@ -523,7 +551,7 @@ def test_api_unimplemented(tmpdir, monkeypatch): flags=re.MULTILINE) with pytest.raises(NotImplementedError) as err: - config(config_file, content) + get_config(config_file, content) assert ("file contains a UNIMPLEMENTED section but no Config " "sub-class has been implemented for this API" in str(err.value)) @@ -540,7 +568,7 @@ def test_default_api(tmpdir): _CONFIG_CONTENT, flags=re.MULTILINE) - default_config = config(config_file, content) + default_config = get_config(config_file, content) assert default_config.api == "dynamo0.3" @@ -566,12 +594,25 @@ def test_root_name_load(tmpdir, content, result): ''' config_file = tmpdir.join("config") - test_config = config(config_file, content) + test_config = get_config(config_file, content) assert test_config._psyir_root_name == result assert test_config.psyir_root_name == result +def test_enable_backend_checks_setter_getter(): + ''' + Test the setter/getter for the 'backend_checks_enabled' property. + ''' + config = Config() + with pytest.raises(TypeError) as err: + config.backend_checks_enabled = "hllo" + assert ("backend_checks_enabled must be a boolean but got 'str'" in + str(err.value)) + config.backend_checks_enabled = True + assert config.backend_checks_enabled is True + + def test_kernel_naming_setter(): ''' Check that the setter for the kernel-naming scheme rejects unrecognised values. @@ -631,7 +672,7 @@ def test_invalid_access_mapping(tmpdir): content = re.sub(r"gh_read: read", "gh_read: invalid", _CONFIG_CONTENT) with pytest.raises(ConfigurationError) as cerr: - config(config_file, content) + get_config(config_file, content) assert "Unknown access type 'invalid' found for key 'gh_read'" \ in str(cerr.value) @@ -648,7 +689,7 @@ def test_default_access_mapping(tmpdir): ''' config_file = tmpdir.join("config") - test_config = config(config_file, _CONFIG_CONTENT) + test_config = get_config(config_file, _CONFIG_CONTENT) api_config = test_config.api_conf("dynamo0.3") for access_mode in api_config.get_access_mapping().values(): @@ -667,7 +708,7 @@ def test_access_mapping_order(tmpdir): content = re.sub(r"gh_inc: inc, gh_sum: sum", "gh_sum: sum, gh_inc: inc", content) - api_config = config(config_file, content).get().api_conf("dynamo0.3") + api_config = get_config(config_file, content).get().api_conf("dynamo0.3") for access_mode in api_config.get_access_mapping().values(): assert isinstance(access_mode, AccessType) @@ -677,7 +718,7 @@ def test_psy_data_prefix(tmpdir): ''' Check the handling of PSyData class prefixes. ''' config_file = tmpdir.join("config.correct") - test_config = config(config_file, _CONFIG_CONTENT) + test_config = get_config(config_file, _CONFIG_CONTENT) assert "profile" in test_config.valid_psy_data_prefixes assert "extract" in test_config.valid_psy_data_prefixes @@ -689,7 +730,7 @@ def test_psy_data_prefix(tmpdir): content = re.sub(r"VALID_PSY_DATA_PREFIXES", "NO-PSY-DATA", _CONFIG_CONTENT) - test_config = config(config_file, content) + test_config = get_config(config_file, content) assert not test_config.valid_psy_data_prefixes @@ -706,7 +747,7 @@ def test_invalid_prefix(tmpdir): _CONFIG_CONTENT, flags=re.MULTILINE) with pytest.raises(ConfigurationError) as err: - config(config_file, content) + get_config(config_file, content) # When there is a '"' in the invalid prefix, the "'" in the # error message is escaped with a '\'. So in order to test the # invalid 'cd"' prefix, we need to have two tests in the assert: diff --git a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py index 6039c78cae..9cca703802 100644 --- a/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py +++ b/src/psyclone/tests/domain/gocean/transformations/gocean1p0_transformations_test.py @@ -1277,9 +1277,10 @@ def test_accloop(tmpdir, fortran_writer): # enclosing parallel region with pytest.raises(GenerationError) as err: _ = fortran_writer(psy.container) - assert ("ACCLoopDirective must have an ACCParallelDirective or " - "ACCKernelsDirective as an ancestor in the Schedule" in - str(err.value)) + assert ("ACCLoopDirective in routine 'invoke_0' must either have an " + "ACCParallelDirective or ACCKernelsDirective as an ancestor in " + "the Schedule or the routine must contain an ACCRoutineDirective" + in str(err.value)) # Add an enclosing parallel region accpara.apply(schedule.children) diff --git a/src/psyclone/tests/domain/lfric/kernel_interface_test.py b/src/psyclone/tests/domain/lfric/kernel_interface_test.py index 465a973b9f..e40b91e4c8 100644 --- a/src/psyclone/tests/domain/lfric/kernel_interface_test.py +++ b/src/psyclone/tests/domain/lfric/kernel_interface_test.py @@ -775,14 +775,78 @@ def test_diff_basis(): assert diff_basis_symbol.shape[3].upper.symbol is nqpv_symbol -@pytest.mark.xfail(reason="Issue #928: this callback is not yet implemented") -def test_field_bcs_kernel(): +def test_field_bcs_kernel(monkeypatch): '''Test that the KernelInterface class field_bcs_kernel method adds the expected symbols to the symbol table and the _arglist list. ''' - kernel_interface = KernelInterface(None) + _, invoke_info = parse(os.path.join( + BASE_PATH, "12.2_enforce_bc_kernel.f90"), + api="dynamo0.3") + psy = PSyFactory("dynamo0.3", + distributed_memory=False).create(invoke_info) + schedule = psy.invokes.invoke_list[0].schedule + kernel = schedule[0].loop_body[0] + kernel_interface = KernelInterface(kernel) kernel_interface.field_bcs_kernel(None) + fld_name = kernel.arguments.args[0].name + fspace = kernel.arguments.unique_fss[0] + fs_name = fspace.orig_name + # ndf declared + ndf_symbol = kernel_interface._symtab.lookup(f"ndf_{fs_name}") + assert isinstance(ndf_symbol, LFRicTypes("NumberOfDofsDataSymbol")) + assert isinstance(ndf_symbol.interface, ArgumentInterface) + assert (ndf_symbol.interface.access == + kernel_interface._read_access.access) + # vertical-boundary dofs mask declared + mask_sym = kernel_interface._symtab.lookup(f"boundary_dofs_{fld_name}") + assert isinstance(mask_sym, + LFRicTypes("VerticalBoundaryDofMaskDataSymbol")) + assert isinstance(mask_sym.interface, ArgumentInterface) + assert mask_sym.interface.access == kernel_interface._read_access.access + assert len(mask_sym.shape) == 2 + assert isinstance(mask_sym.shape[0].upper, Reference) + assert mask_sym.shape[0].upper.symbol is ndf_symbol + assert isinstance(mask_sym.shape[1].upper, Literal) + assert mask_sym.shape[1].upper.value == "2" + + +def test_field_bcs_kernel_errors(monkeypatch): + ''' + Test that the field_bcs_kernel method raises the expected errors if the + kernel does not have exactly one argument that is itself a field on the + 'ANY_SPACE_1' function space. + + ''' + _, invoke_info = parse(os.path.join( + BASE_PATH, "1_single_invoke.f90"), api="dynamo0.3") + psy = PSyFactory("dynamo0.3", + distributed_memory=False).create(invoke_info) + schedule = psy.invokes.invoke_list[0].schedule + kernel = schedule[0].loop_body[0] + kernel_interface = KernelInterface(kernel) + with pytest.raises(InternalError) as err: + kernel_interface.field_bcs_kernel(None) + assert ("Kernel 'testkern_code' applies boundary conditions to a field " + "and therefore should have a single, field argument (one of " + "['gh_field']) but got ['gh_scalar', 'gh_field'" in str(err.value)) + # Repeat for a kernel that *does* have an argument on ANY_SPACE_1. + _, invoke_info = parse(os.path.join( + BASE_PATH, "12.2_enforce_bc_kernel.f90"), api="dynamo0.3") + psy = PSyFactory("dynamo0.3", + distributed_memory=False).create(invoke_info) + schedule = psy.invokes.invoke_list[0].schedule + kernel = schedule[0].loop_body[0] + # Monkeypatch the argument so that it appears to be on the wrong space. + monkeypatch.setattr( + kernel.arguments._args[0]._function_spaces[0], + "_orig_name", "ANY_SPACE_2") + kernel_interface = KernelInterface(kernel) + with pytest.raises(InternalError) as err: + kernel_interface.field_bcs_kernel(None) + assert ("Kernel 'enforce_bc_code' applies boundary conditions to a field " + "but the supplied argument, 'a', is on 'ANY_SPACE_2' rather than " + "the expected 'ANY_SPACE_1'" in str(err.value)) @pytest.mark.xfail(reason="Issue #928: this callback is not yet implemented") diff --git a/src/psyclone/tests/domain/lfric/lfric_types_test.py b/src/psyclone/tests/domain/lfric/lfric_types_test.py index 721ffc74cf..50681ed4fd 100644 --- a/src/psyclone/tests/domain/lfric/lfric_types_test.py +++ b/src/psyclone/tests/domain/lfric/lfric_types_test.py @@ -146,8 +146,8 @@ def test_scalar_literals(): assert isinstance(LFRicTypes("LFRicDimension")("3"), lfric_dim_class) with pytest.raises(ValueError) as info: - LFRicTypes("LFRicDimension")("2") - assert ("An LFRic dimension object must be '1' or '3', but found '2'." + LFRicTypes("LFRicDimension")("4") + assert ("An LFRic dimension object must be '1', '2' or '3', but found '4'." in str(info.value)) # LFRIC_SCALAR_DIMENSION instance assert isinstance(LFRicTypes("LFRIC_SCALAR_DIMENSION"), lfric_dim_class) diff --git a/src/psyclone/tests/domain/lfric/metadata_to_arguments_rules_test.py b/src/psyclone/tests/domain/lfric/metadata_to_arguments_rules_test.py index 1eeaeb74ce..9b391ef122 100644 --- a/src/psyclone/tests/domain/lfric/metadata_to_arguments_rules_test.py +++ b/src/psyclone/tests/domain/lfric/metadata_to_arguments_rules_test.py @@ -70,6 +70,24 @@ def check_called(monkeypatch, function, method_name, metadata): assert method_name in str(info.value) +def test_bc_kern_regex(): + ''' + Test the regular expression used to identify the boundary-condition kernel + and its transformed forms. + + TODO #487 - this test should be removed once metadata is used to identify + the boundary-condition kernel. + + ''' + cls = MetadataToArgumentsRules + assert cls.bc_kern_regex.match("enforce_bc_code") + assert cls.bc_kern_regex.match("enforce_BC_code") + assert cls.bc_kern_regex.match("enforce_BC_1099_code") + assert not cls.bc_kern_regex.match("other_bc_code") + assert not cls.bc_kern_regex.match("enforce_bc_a1_code") + assert not cls.bc_kern_regex.match("enforce_bc_1a_code") + + def test_mapping(monkeypatch): '''Test the MetadataToArgumentRules class mapping method works as expected. diff --git a/src/psyclone/tests/domain/nemo/transformations/nemo_arrayrange2loop_trans_test.py b/src/psyclone/tests/domain/nemo/transformations/nemo_arrayrange2loop_trans_test.py index 119f85ea9a..b706e6dada 100644 --- a/src/psyclone/tests/domain/nemo/transformations/nemo_arrayrange2loop_trans_test.py +++ b/src/psyclone/tests/domain/nemo/transformations/nemo_arrayrange2loop_trans_test.py @@ -283,6 +283,30 @@ def test_apply_structure_of_arrays_multiple_arrays(fortran_reader, " enddo\n" in result) +def test_transform_apply_fixed(): + '''Check that the PSyIR is transformed as expected for a lat,lon loop + when the lhs of the loop has known fixed bounds with the same + values for each index. There used to be a bug where the first + index was picked up in error instead of the second in the + arrayrange2loop validate method but this should now be fixed. + + ''' + _, invoke_info = get_invoke("fixed_lhs.f90", api=API, idx=0) + schedule = invoke_info.schedule + assignment = schedule[0] + range_node = assignment.lhs.children[1] + trans = NemoArrayRange2LoopTrans() + trans.apply(range_node) + writer = FortranWriter() + result = writer(schedule) + print(result) + expected = ( + " do idx = 6, 8, 1\n" + " sshn(2:4,idx) = sshn(2:4,idx) + ssh_ref * tmask(2:4,idx)\n" + " enddo\n") + assert expected in result + + def test_validate_unsupported_structure_of_arrays(fortran_reader): '''Check that nested structure_of_arrays are not supported. ''' trans = NemoArrayRange2LoopTrans() diff --git a/src/psyclone/tests/f2pygen_test.py b/src/psyclone/tests/f2pygen_test.py index 95c3228203..92c34b484a 100644 --- a/src/psyclone/tests/f2pygen_test.py +++ b/src/psyclone/tests/f2pygen_test.py @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # BSD 3-Clause License # -# Copyright (c) 2017-2022, Science and Technology Facilities Council +# Copyright (c) 2017-2023, Science and Technology Facilities Council # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -35,12 +35,13 @@ ''' Tests for the f2pygen module of PSyclone ''' -from __future__ import absolute_import, print_function import pytest -from psyclone.f2pygen import ModuleGen, CommentGen, SubroutineGen, DoGen, \ - CallGen, AllocateGen, DeallocateGen, IfThenGen, DeclGen, TypeDeclGen, \ - CharDeclGen, ImplicitNoneGen, UseGen, DirectiveGen, AssignGen, PSyIRGen -from psyclone.errors import InternalError +from psyclone.configuration import Config +from psyclone.f2pygen import ( + adduse, AssignGen, AllocateGen, BaseGen, CallGen, CharDeclGen, CommentGen, + DeallocateGen, DeclGen, DirectiveGen, DoGen, IfThenGen, ImplicitNoneGen, + ModuleGen, PSyIRGen, SelectionGen, SubroutineGen, TypeDeclGen, UseGen) +from psyclone.errors import GenerationError, InternalError from psyclone.psyir.nodes import Node, Return from psyclone.tests.utilities import Compile, count_lines, line_number @@ -615,7 +616,6 @@ def test_ompdirective_type(): def test_basegen_add_auto(): ''' Check that attempting to call add on BaseGen raises an error if position is "auto"''' - from psyclone.f2pygen import BaseGen parent = Node() bgen = BaseGen(parent, parent) obj = Node() @@ -627,7 +627,6 @@ def test_basegen_add_auto(): def test_basegen_add_invalid_posn(): '''Check that attempting to call add on BaseGen with an invalid position argument raises an error''' - from psyclone.f2pygen import BaseGen parent = Node() bgen = BaseGen(parent, parent) obj = Node() @@ -651,7 +650,6 @@ def test_basegen_append(): def test_basegen_append_default(): ''' Check if no position argument is supplied to BaseGen.add() then it defaults to appending ''' - from psyclone.f2pygen import BaseGen module = ModuleGen(name="testmodule") sub = SubroutineGen(module, name="testsubroutine") module.add(sub) @@ -910,7 +908,6 @@ def test_adduse_empty_only(): module = ModuleGen(name="testmodule") sub = SubroutineGen(module, name="testsubroutine") module.add(sub) - from psyclone.f2pygen import adduse # Add a use statement with only=True but an empty list of entities adduse("fred", sub.root, only=True, funcnames=[]) assert count_lines(sub.root, "USE fred") == 1 @@ -925,7 +922,6 @@ def test_adduse(): module.add(sub) call = CallGen(sub, name="testcall", args=["a", "b"]) sub.add(call) - from psyclone.f2pygen import adduse adduse("fred", call.root, only=True, funcnames=["astaire"]) gen = str(sub.root) expected = (" SUBROUTINE testsubroutine()\n" @@ -941,7 +937,6 @@ def test_adduse_default_funcnames(): module.add(sub) call = CallGen(sub, name="testcall", args=["a", "b"]) sub.add(call) - from psyclone.f2pygen import adduse adduse("fred", call.root) gen = str(sub.root) expected = (" SUBROUTINE testsubroutine()\n" @@ -1359,7 +1354,6 @@ def test_declgen_multiple_use2(): @pytest.mark.xfail(reason="No way to add body of DEFAULT clause") def test_selectiongen(): ''' Check that SelectionGen works as expected ''' - from psyclone.f2pygen import SelectionGen module = ModuleGen(name="testmodule") sub = SubroutineGen(module, name="testsubroutine") module.add(sub) @@ -1383,7 +1377,6 @@ def test_selectiongen(): def test_selectiongen_addcase(): ''' Check that SelectionGen.addcase() works as expected when no content is supplied''' - from psyclone.f2pygen import SelectionGen module = ModuleGen(name="testmodule") sub = SubroutineGen(module, name="testsubroutine") module.add(sub) @@ -1401,7 +1394,6 @@ def test_selectiongen_addcase(): @pytest.mark.xfail(reason="Adding a CASE to a SELECT TYPE does not work") def test_typeselectiongen(): ''' Check that SelectionGen works as expected for a type ''' - from psyclone.f2pygen import SelectionGen module = ModuleGen(name="testmodule") sub = SubroutineGen(module, name="testsubroutine") module.add(sub) @@ -1523,3 +1515,34 @@ def test_psyirgen_multiple_fparser_nodes(): END MODULE testmodule''' assert generated_code == expected + + +def test_psyirgen_backendchecks(monkeypatch): + '''Check that PSyIRGen uses the configuration object to determine + whether or not to disable checks in the PSyIR backend. + ''' + config = Config.get() + + module = ModuleGen(name="testmodule") + subroutine = SubroutineGen(module, name="testsubroutine") + module.add(subroutine) + node = Return() + + # monkeypatch the `validate_global_constraints` method of the Return node + # so that it always raises an error. + def fake_validate(): + raise GenerationError("This is just a test") + + monkeypatch.setattr(node, "validate_global_constraints", fake_validate) + + # monkeypatch Config to turn off validation checks. + monkeypatch.setattr(config, "_backend_checks_enabled", False) + # Constructing the PSyIRGen node should succed. + pgen = PSyIRGen(subroutine, node) + assert isinstance(pgen, PSyIRGen) + # monkeypatch Config to turn on validation checks. + monkeypatch.setattr(config, "_backend_checks_enabled", True) + # Construction should now fail. + with pytest.raises(GenerationError) as err: + PSyIRGen(subroutine, node) + assert "This is just a test" in str(err.value) diff --git a/src/psyclone/tests/generator_test.py b/src/psyclone/tests/generator_test.py index 563d7dfe15..61c8758e4c 100644 --- a/src/psyclone/tests/generator_test.py +++ b/src/psyclone/tests/generator_test.py @@ -727,6 +727,26 @@ def test_main_directory_arg(capsys): "-d", NEMO_BASE_PATH]) +def test_main_disable_backend_validation_arg(capsys): + '''Test the --backend option in main().''' + filename = os.path.join(DYN03_BASE_PATH, "1_single_invoke.f90") + with pytest.raises(SystemExit): + main([filename, "--backend", "invalid"]) + _, output = capsys.readouterr() + assert "--backend: invalid choice: 'invalid'" in output + + # Make sure we get a default config instance + Config._instance = None + # Default is to have checks enabled. + assert Config.get().backend_checks_enabled is True + main([filename, "--backend", "disable-validation"]) + assert Config.get().backend_checks_enabled is False + Config._instance = None + main([filename, "--backend", "enable-validation"]) + assert Config.get().backend_checks_enabled is True + Config._instance = None + + def test_main_expected_fatal_error(capsys): '''Tests that we get the expected output and the code exits with an error when an expected fatal error is returned from the generate diff --git a/src/psyclone/tests/nemo/test_files/fixed_lhs.f90 b/src/psyclone/tests/nemo/test_files/fixed_lhs.f90 new file mode 100644 index 0000000000..035ba0c021 --- /dev/null +++ b/src/psyclone/tests/nemo/test_files/fixed_lhs.f90 @@ -0,0 +1,42 @@ +! ----------------------------------------------------------------------------- +! BSD 3-Clause License +! +! Copyright (c) 2023, 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. +! ----------------------------------------------------------------------------- +! Author R. W. Ford, STFC Daresbury Lab + +program fixed_lhs + implicit none + real, dimension(10,10) :: sshn + real, dimension(:,:) :: tmask + real :: ssh_ref + + sshn(2:4,6:8) = sshn(2:4,6:8) + ssh_ref * tmask(2:4,6:8) + +end program fixed_lhs diff --git a/src/psyclone/tests/nemo/transformations/openacc/loop_directive_test.py b/src/psyclone/tests/nemo/transformations/openacc/loop_directive_test.py index 5e2c51c2e3..5b5c29fb9a 100644 --- a/src/psyclone/tests/nemo/transformations/openacc/loop_directive_test.py +++ b/src/psyclone/tests/nemo/transformations/openacc/loop_directive_test.py @@ -70,8 +70,10 @@ def test_missing_enclosing_region(parser): acc_trans.apply(schedule[0]) with pytest.raises(GenerationError) as err: str(psy.gen) - assert ("ACCLoopDirective must have an ACCParallelDirective or " - "ACCKernelsDirective as an ancestor" in str(err.value)) + assert ("ACCLoopDirective in routine 'do_loop' must either have an " + "ACCParallelDirective or ACCKernelsDirective as an ancestor in " + "the Schedule or the routine must contain an ACCRoutineDirective" + in str(err.value)) def test_explicit_loop(parser): diff --git a/src/psyclone/tests/psyir/nodes/acc_directives_test.py b/src/psyclone/tests/psyir/nodes/acc_directives_test.py index abbbb3bb09..93b3efa689 100644 --- a/src/psyclone/tests/psyir/nodes/acc_directives_test.py +++ b/src/psyclone/tests/psyir/nodes/acc_directives_test.py @@ -63,8 +63,9 @@ from psyclone.psyir.nodes.loop import Loop from psyclone.psyir.nodes.schedule import Schedule from psyclone.psyir.symbols import SymbolTable, DataSymbol, INTEGER_TYPE -from psyclone.transformations import (ACCDataTrans, ACCEnterDataTrans, - ACCParallelTrans, ACCKernelsTrans) +from psyclone.transformations import ( + ACCDataTrans, ACCEnterDataTrans, ACCKernelsTrans, ACCLoopTrans, + ACCParallelTrans, ACCRoutineTrans) BASE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname( os.path.abspath(__file__)))), "test_files", "dynamo0p3") @@ -307,6 +308,50 @@ def test_accloopdirective_equality(): directive2._vector = not directive1._vector assert directive1 != directive2 + +def test_accloopdirective_validate(fortran_reader): + ''' + Check the validate_global_constraints method of ACCLoopDirective. For + an ACC loop to validate, it must either be within an 'ACC parallel/kernels' + region or in a routine with an 'ACC routine' directive. + + ''' + code = '''\ +subroutine my_sub() + implicit none + real, dimension(10,10) :: var + integer :: ji, jj + do jj = 1, 10 + do ji = 1, 10 + var(ji,jj) = ji + jj + end do + end do +end subroutine my_sub''' + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + # Add an orphan ACC loop directive. + acclooptrans = ACCLoopTrans() + acclooptrans.apply(routine[0]) + # This should be rejected. + with pytest.raises(GenerationError) as err: + routine[0].validate_global_constraints() + assert ("ACCLoopDirective in routine 'my_sub' must either have an " + "ACCParallelDirective or ACCKernelsDirective as an ancestor in " + "the Schedule or the routine must contain an ACCRoutineDirective." + in str(err.value)) + # Add an ACCRoutineDirective. + accrtrans = ACCRoutineTrans() + accrtrans.apply(routine) + routine[0].validate_global_constraints() + # Remove the ACCRoutineDirective. + routine.children.pop(index=0) + with pytest.raises(GenerationError) as err: + routine[0].validate_global_constraints() + # Add an ACC Parallel region + accptrans = ACCParallelTrans() + accptrans.apply(routine.children) + routine[0].validate_global_constraints() + # Class ACCLoopDirective end diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/array_reduction_base_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/array_reduction_base_trans_test.py new file mode 100644 index 0000000000..b093b5e40d --- /dev/null +++ b/src/psyclone/tests/psyir/transformations/intrinsics/array_reduction_base_trans_test.py @@ -0,0 +1,648 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023, 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. +# ----------------------------------------------------------------------------- +# Author: R. W. Ford, STFC Daresbury Laboratory + +'''Module containing tests for the array_reduction_base_trans which is +an abstract parent class for the array-reduction intrinsic +transformations. + +''' +import pytest + +from psyclone.psyir.nodes import IntrinsicCall, Reference, Literal +from psyclone.psyir.symbols import ( + Symbol, BOOLEAN_TYPE, INTEGER_TYPE, DataSymbol, REAL_TYPE) +from psyclone.psyir.transformations import ( + TransformationError, Maxval2LoopTrans) +from psyclone.psyir.transformations.intrinsics.array_reduction_base_trans \ + import ArrayReductionBaseTrans +from psyclone.tests.utilities import Compile + + +def test_init_exception(): + '''Check that this class can't be created as it is abstract.''' + # pylint: disable=abstract-class-instantiated + with pytest.raises(TypeError) as info: + _ = ArrayReductionBaseTrans() + # Python >= 3.12 tweaks the error message to mention + # the lack of an implementation and to quote the method names. + # We split the check to accomodate for this. + assert ("Can't instantiate abstract class ArrayReductionBaseTrans with" + in str(info.value)) + assert "abstract methods" in str(info.value) + assert "_init_var" in str(info.value) + assert "_loop_body" in str(info.value) + + +def test_get_args(): + '''Check the _get_args static method works as expected.''' + # array + array_reference = Reference(Symbol("array")) + node = IntrinsicCall.create(IntrinsicCall.Intrinsic.SUM, [array_reference]) + result = ArrayReductionBaseTrans._get_args(node) + assert result == (array_reference, None, None) + + # array, mask, dim + mask_reference = Literal("true", BOOLEAN_TYPE) + dim_reference = Literal("1", INTEGER_TYPE) + node = IntrinsicCall.create(IntrinsicCall.Intrinsic.SUM, [ + array_reference.copy(), ("mask", mask_reference), + ("dim", dim_reference)]) + result = ArrayReductionBaseTrans._get_args(node) + assert result == (array_reference, dim_reference, mask_reference) + + +def test_str(): + ''' Check that the __str__ method behaves as expected. ''' + assert str(Maxval2LoopTrans()) == ("Convert the PSyIR MAXVAL intrinsic to " + "equivalent PSyIR code.") + + +# validate method + +def test_validate_node(): + '''Check that an incorrect node raises the expected exception.''' + trans = Maxval2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(None) + assert ("Error in Maxval2LoopTrans transformation. The supplied node " + "argument is not an intrinsic, found 'NoneType'." + in str(info.value)) + + intrinsic = IntrinsicCall.create( + IntrinsicCall.Intrinsic.MINVAL, + [Reference(DataSymbol("array", REAL_TYPE))]) + with pytest.raises(TransformationError) as info: + trans.validate(intrinsic) + assert ("The supplied node argument is not a maxval intrinsic, found " + "'MINVAL'." in str(info.value)) + + +def test_structure_error(fortran_reader): + '''Test that the transformation raises an exception if the array node + is part of a structure and has no array references within the + array node. + + ''' + code = ( + "subroutine test(n,m)\n" + " integer :: n, m\n" + " type :: array_type\n" + " real :: array(10,10)\n" + " end type\n" + " type(array_type) :: ref\n" + " real :: result\n" + " integer :: dimension\n" + " result = maxval(ref%array)\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0].children[1] + trans = Maxval2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(node) + assert ("Error, no ArrayReference's found in the expression 'ref%array'." + in str(info.value)) + + +def test_lhs(fortran_reader): + '''Test that an exception is raised when an array-reduction intrinsic + is on the LHS of an asssignment. Uses the Maxval2LoopTrans + transformation (a subclass of ArrayReductionBaseTrans), as it is + easier to test. + + ''' + code = ( + "subroutine test(array)\n" + " real :: array(10)\n" + " real :: result(10)\n" + " result(maxval(array)) = 0.0\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[0].children[0] + with pytest.raises(TransformationError) as info: + trans.validate(node) + assert ("Error, intrinsics on the lhs of an assignment are not currently " + "supported." in str(info.value)) + + +def test_array_shape(fortran_reader, monkeypatch): + '''Tests that the expected exception is raised if the array range is + not a valid value. Requires monkeypatching. + + ''' + code = ( + "subroutine test(array,n,m)\n" + " integer :: n, m\n" + " real :: array(1)\n" + " real :: result\n" + " result = maxval(array)\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + + # Modify array shape from node to create exception + array_ref = node.children[0] + array_symbol = array_ref.symbol + monkeypatch.setattr(array_symbol._datatype, "_shape", [None]) + + trans = Maxval2LoopTrans() + with pytest.raises(TypeError) as info: + trans.validate(node) + assert ("ArrayType shape-list elements can only be 'int', " + "ArrayType.Extent, 'DataNode' or a 2-tuple thereof but found " + "'NoneType'." in str(info.value)) + + +def test_unexpected_shape(fortran_reader, monkeypatch): + '''Tests that the expected exception is raised if the array shape is + not a valid value. Requires monkeypatching. + + ''' + code = ( + "subroutine test(array,n,m)\n" + " integer :: n, m\n" + " real :: array(1)\n" + " real :: result\n" + " result = maxval(array)\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + array_ref = node.children[0] + # Modify the shape of the array reference shape to create an + # exception + monkeypatch.setattr(array_ref.symbol._datatype, "_shape", [1]) + + trans = Maxval2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(node) + assert ("Unexpected shape for array. Expecting one of Deferred, Attribute " + "or Bounds but found '1'." in str(info.value)) + + +def test_not_assignment(fortran_reader): + '''Test that the expected exception is raised if the intrinsic call is + not part of an assignment (e.g. is an argument to a subroutine), + as this is not currently supported. + + ''' + code = ( + "subroutine test(array)\n" + " integer :: array(10)\n" + " call routine(maxval(array))\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Call/IntrinsicCall + node = psyir.children[0].children[0].children[0] + trans = Maxval2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(node) + assert ("Maxval2LoopTrans only works when the intrinsic is part " + "of an Assignment" in str(info.value)) + + +# apply + +@pytest.mark.parametrize("idim1,idim2,rdim11,rdim12,rdim21,rdim22", + [("10", "20", "1", "10", "1", "20"), + ("n", "m", "1", "n", "1", "m"), + ("0:n", "2:m", "0", "n", "2", "m"), + (":", ":", "LBOUND(array, dim=1)", + "UBOUND(array, dim=1)", + "LBOUND(array, dim=2)", + "UBOUND(array, dim=2)")]) +def test_apply(idim1, idim2, rdim11, rdim12, rdim21, rdim22, + fortran_reader, fortran_writer, tmpdir): + '''Test that a maxval intrinsic as the only term on the rhs of an + assignment with a single array argument gets transformed as + expected. Test with known and unknown array sizes. What we care + about here are the initialisation of the result variable, the + generated intrinsic (MAX) and the loop bounds. + + ''' + code = ( + f"subroutine test(array,n,m)\n" + f" integer :: n,m\n" + f" real :: array({idim1},{idim2})\n" + f" real :: result\n" + f" result = maxval(array)\n" + f"end subroutine\n") + expected = ( + f" result = TINY(result)\n" + f" do idx = {rdim21}, {rdim22}, 1\n" + f" do idx_1 = {rdim11}, {rdim12}, 1\n" + f" result = MAX(result, array(idx_1,idx))\n" + f" enddo\n" + f" enddo\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.walk(IntrinsicCall)[0] + trans = Maxval2LoopTrans() + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_apply_multi(fortran_reader, fortran_writer, tmpdir): + '''Test that a MAXVAL intrinsic as part of multiple term on the rhs of + an assignment with a single array argument gets transformed as + expected. What we care about here are the initialisation of the + result variable, the generated intrinsic (MAX) and the loop + bounds. + + ''' + code = ( + "subroutine test(array,n,m,value1,value2)\n" + " integer :: n, m\n" + " real :: array(n,m)\n" + " real :: value1, value2\n" + " real :: result\n" + " result = value1 + maxval(array) * value2\n" + "end subroutine\n") + expected = ( + " result = TINY(result)\n" + " do idx = 1, m, 1\n" + " do idx_1 = 1, n, 1\n" + " result = MAX(result, array(idx_1,idx))\n" + " enddo\n" + " enddo\n" + " result = value1 + result * value2\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ + # BinaryOperation(MUL)/IntrinsicCall + node = psyir.walk(IntrinsicCall)[0] + trans = Maxval2LoopTrans() + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_apply_dimension_1d(fortran_reader): + '''Test that the apply method works as expected when a dimension + argument is specified and the array is one dimensional. This + should be the same as if dimension were not specified at all. + What we care about here are the initialisation of the result + variable, the generated intrinsic (MAX) and the loop bounds. + + ''' + code = ( + "subroutine test(array)\n" + " real :: array(:)\n" + " real :: result\n" + " result = maxval(array,dim=1)\n" + "end subroutine\n") + psyir = fortran_reader.psyir_from_source(code) + node = psyir.walk(IntrinsicCall)[0] + trans = Maxval2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(node) + assert ("The dimension argument to MAXVAL is not yet supported." + in str(info.value)) + + +def test_mask(fortran_reader, fortran_writer, tmpdir): + '''Test that the transformation works when there is a mask specified. + What we care about here are the initialisation of the result + variable, the generated intrinsic (MAX) and the loop bounds. + + ''' + code = ( + "program test\n" + " real :: array(10,10)\n" + " real :: result\n" + " result = maxval(array, mask=MOD(array, 2.0)==1)\n" + "end program\n") + expected = ( + " result = TINY(result)\n" + " do idx = 1, 10, 1\n" + " do idx_1 = 1, 10, 1\n" + " if (MOD(array(idx_1,idx), 2.0) == 1) then\n" + " result = MAX(result, array(idx_1,idx))\n" + " end if\n" + " enddo\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.walk(IntrinsicCall)[0] + trans = Maxval2LoopTrans() + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_mask_array_indexed(fortran_reader, fortran_writer, tmpdir): + '''Test that the mask code works if the array iself it used as part of + the mask. In this case it will already be indexed. Use the + Maxval2LoopTrans transformation (a subclass of + ArrayReductionBaseTrans), as it is easier to test. + + ''' + code = ( + "program sum_test\n" + " real :: a(4)\n" + " real :: result\n" + " a(1) = 2.0\n" + " a(2) = 1.0\n" + " a(3) = 2.0\n" + " a(4) = 1.0\n" + " result = maxval(a, mask=a(1)>a)\n" + "end program\n") + expected = ( + " result = TINY(result)\n" + " do idx = 1, 4, 1\n" + " if (a(1) > a(idx)) then\n" + " result = MAX(result, a(idx))\n" + " end if\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.walk(IntrinsicCall)[0] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_allocate(fortran_reader, fortran_writer, tmpdir): + '''Test that a newly created array is allocated after the original + array is allocated (if the original array is allocated). Use the + Maxval2LoopTrans transformations (a subclass of + ArrayReductionBaseTrans), as it is easier to test. + + ''' + code = ( + "program sum_test\n" + " real, allocatable :: a(:,:,:)\n" + " real :: result(4,4)\n" + " allocate(a(4,4,4))\n" + " result = maxval(a)\n" + " deallocate(a)\n" + "end program\n") + expected = ( + " ALLOCATE(a(1:4,1:4,1:4))\n" + " result = TINY(result)\n" + " do idx = LBOUND(a, dim=3), UBOUND(a, dim=3), 1\n" + " do idx_1 = LBOUND(a, dim=2), UBOUND(a, dim=2), 1\n" + " do idx_2 = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" + " result = MAX(result, a(idx_2,idx_1,idx))\n" + " enddo\n" + " enddo\n" + " enddo\n" + " DEALLOCATE(a)\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.walk(IntrinsicCall)[1] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_references(fortran_reader, fortran_writer, tmpdir): + '''Test that the expected result is obtained when the arrays in the + expression and mask have References. These are internally changed + to ArrayReferences using the Reference2ArrayRangeTrans + transformation where required. + + ''' + code = ( + "subroutine test(tmask)\n" + "real :: sshn(10,10), tmask(:,:)\n" + "real :: zmax(10)\n" + "real :: ssh_ref\n" + "zmax(1) = MAXVAL(ABS(sshn + ssh_ref * tmask), mask=tmask==1.0)\n" + "end subroutine\n") + expected = ( + " zmax(1) = TINY(zmax)\n" + " do idx = 1, 10, 1\n" + " do idx_1 = 1, 10, 1\n" + " if (tmask(idx_1,idx) == 1.0) then\n" + " zmax(1) = MAX(zmax(1), ABS(sshn(idx_1,idx) + ssh_ref * " + "tmask(idx_1,idx)))\n" + " end if\n" + " enddo\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_nemo_example(fortran_reader, fortran_writer, tmpdir): + '''Test a typical nemo example produces the expected code.''' + code = ( + "subroutine test()\n" + "real :: sshn(10,10), tmask(10,10,10)\n" + "real :: zmax(10)\n" + "real :: ssh_ref\n" + "zmax(1) = MAXVAL(ABS(sshn(:,:) + ssh_ref * tmask(:,:,1)))\n" + "end subroutine\n") + expected = ( + " zmax(1) = TINY(zmax)\n" + " do idx = LBOUND(sshn, dim=2), UBOUND(sshn, dim=2), 1\n" + " do idx_1 = LBOUND(sshn, dim=1), UBOUND(sshn, dim=1), 1\n" + " zmax(1) = MAX(zmax(1), ABS(sshn(idx_1,idx) + ssh_ref * " + "tmask(idx_1,idx,1)))\n" + " enddo\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_constant_dims(fortran_reader, fortran_writer, tmpdir): + '''Test that the code works as expected when one of more of the arrays + contains a dimension with a constant value rather than a range. + + ''' + code = ( + "subroutine test()\n" + "real :: a(10,10), b(10,10), c(10)\n" + "real :: x\n" + "x = maxval(a(:,1)+b(10,:), mask=c(:)==1.0)\n" + "end subroutine\n") + expected = ( + " x = TINY(x)\n" + " do idx = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" + " if (c(idx) == 1.0) then\n" + " x = MAX(x, a(idx,1) + b(10,idx))\n" + " end if\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_expression_1d(fortran_reader, fortran_writer, tmpdir): + '''Check that the expected code is produced when the arrays in the + expressions are one dimensional. + + ''' + code = ( + "subroutine test()\n" + "real :: a(10), b(10)\n" + "real :: x\n" + "x = maxval(a(:)+b(:))\n" + "end subroutine\n") + expected = ( + "subroutine test()\n" + " real, dimension(10) :: a\n" + " real, dimension(10) :: b\n" + " real :: x\n" + " integer :: idx\n\n" + " x = TINY(x)\n" + " do idx = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" + " x = MAX(x, a(idx) + b(idx))\n" + " enddo\n\n" + "end subroutine test\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + trans.apply(node) + result = fortran_writer(psyir) + assert result == expected + assert Compile(tmpdir).string_compiles(result) + + +def test_expression_3d(fortran_reader, fortran_writer, tmpdir): + '''Check that the expected code is produced when the arrays in the + expressions are multi-dimensional (3 dimensions in this case). + + ''' + code = ( + "subroutine test()\n" + "real :: a(10,10,10)\n" + "real :: x\n" + "x = maxval(-a(:,:,:)+10.0)\n" + "end subroutine\n") + expected = ( + "subroutine test()\n" + " real, dimension(10,10,10) :: a\n" + " real :: x\n" + " integer :: idx\n" + " integer :: idx_1\n" + " integer :: idx_2\n\n" + " x = TINY(x)\n" + " do idx = LBOUND(a, dim=3), UBOUND(a, dim=3), 1\n" + " do idx_1 = LBOUND(a, dim=2), UBOUND(a, dim=2), 1\n" + " do idx_2 = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" + " x = MAX(x, -a(idx_2,idx_1,idx) + 10.0)\n" + " enddo\n" + " enddo\n" + " enddo\n\n" + "end subroutine test\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/IntrinsicCall + node = psyir.children[0].children[0].children[1] + trans.apply(node) + result = fortran_writer(psyir) + assert result == expected + assert Compile(tmpdir).string_compiles(result) + + +def test_multi_intrinsics(fortran_reader, fortran_writer, tmpdir): + '''Check that the expected code is produced when there is more than + one of the same intrinsic on the rhs of the assignment. + + ''' + code = ( + "subroutine test()\n" + "real :: a(10), b(10)\n" + "real :: x\n" + "x = maxval(a(:)) + maxval(b(:))\n" + "end subroutine\n") + expected = ( + " x = TINY(x)\n" + " do idx = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" + " x = MAX(x, a(idx))\n" + " enddo\n" + " x = x + MAXVAL(b(:))\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + # FileContainer/Routine/Assignment/BinaryOp/IntrinsicCall + node = psyir.children[0].children[0].children[1].children[0] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) + + +def test_increment(fortran_reader, fortran_writer, tmpdir): + '''Check that the expected code is produced when the variable being + assigned to is an increment e.g. x = x + ... + + ''' + code = ( + "subroutine test()\n" + "real :: a(10)\n" + "real :: x\n" + "x = x + maxval(a)\n" + "end subroutine\n") + expected = ( + " tmp_var = TINY(tmp_var)\n" + " do idx = 1, 10, 1\n" + " tmp_var = MAX(tmp_var, a(idx))\n" + " enddo\n" + " x = x + tmp_var\n") + psyir = fortran_reader.psyir_from_source(code) + trans = Maxval2LoopTrans() + node = psyir.walk(IntrinsicCall)[0] + trans.apply(node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/maxval2code_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/maxval2loop_trans_test.py similarity index 60% rename from src/psyclone/tests/psyir/transformations/intrinsics/maxval2code_trans_test.py rename to src/psyclone/tests/psyir/transformations/intrinsics/maxval2loop_trans_test.py index d9c9659c0c..5f95f54a4d 100644 --- a/src/psyclone/tests/psyir/transformations/intrinsics/maxval2code_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/intrinsics/maxval2loop_trans_test.py @@ -33,15 +33,14 @@ # ----------------------------------------------------------------------------- # Author: R. W. Ford, STFC Daresbury Laboratory -'''Module containing tests for the maxval2code transformation.''' +'''Module containing tests for the maxval2loop transformation.''' import pytest -from psyclone.psyir.nodes import Reference, ArrayReference -from psyclone.psyir.symbols import ( - REAL_TYPE, DataSymbol, INTEGER_TYPE, ArrayType) +from psyclone.psyir.nodes import Reference, Literal +from psyclone.psyir.symbols import REAL_TYPE, DataSymbol from psyclone.psyir.transformations import ( - Maxval2CodeTrans, TransformationError) + Maxval2LoopTrans, TransformationError) from psyclone.tests.utilities import Compile @@ -50,56 +49,23 @@ def test_initialise(): _INTRINSIC_NAME is set up as expected. ''' - trans = Maxval2CodeTrans() - assert isinstance(trans, Maxval2CodeTrans) + trans = Maxval2LoopTrans() + assert isinstance(trans, Maxval2LoopTrans) assert trans._INTRINSIC_NAME == "MAXVAL" def test_loop_body(): - '''Test that the _loop_body method works as expected, without an array - reduction. - - ''' - trans = Maxval2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - array_iterators = [j_iterator, i_iterator] - var_symbol = DataSymbol("var", REAL_TYPE) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator)]) - result = trans._loop_body(False, array_iterators, var_symbol, array_ref) - assert result.debug_string() == ( - "if (var < array(i,j)) then\n" - " var = array(i,j)\n" - "end if\n") - - -def test_loop_body_reduction(): - '''Test that the _loop_body method works as expected, with an array - reduction. - - ''' - trans = Maxval2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - k_iterator = DataSymbol("k", INTEGER_TYPE) - array_iterators = [i_iterator, k_iterator] - var_symbol = DataSymbol("var", ArrayType(REAL_TYPE, [10, 10])) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator), - Reference(k_iterator)]) - result = trans._loop_body(True, array_iterators, var_symbol, array_ref) - assert result.debug_string() == ( - "if (var(i,k) < array(i,j,k)) then\n" - " var(i,k) = array(i,j,k)\n" - "end if\n") + '''Test that the _loop_body method works as expected.''' + trans = Maxval2LoopTrans() + lhs = Reference(DataSymbol("i", REAL_TYPE)) + rhs = Literal("1.0", REAL_TYPE) + result = trans._loop_body(lhs, rhs) + assert "MAX(i, 1.0)" in result.debug_string() def test_init_var(): '''Test that the _init_var method works as expected.''' - trans = Maxval2CodeTrans() + trans = Maxval2LoopTrans() var_symbol = DataSymbol("var", REAL_TYPE) result = trans._init_var(var_symbol) # As 'tiny' is not yet part of an expression, the 'debug_string()' @@ -112,7 +78,7 @@ def test_str(): as expected. ''' - trans = Maxval2CodeTrans() + trans = Maxval2LoopTrans() assert str(trans) == ("Convert the PSyIR MAXVAL intrinsic to equivalent " "PSyIR code.") @@ -122,8 +88,8 @@ def test_name(): as expected. ''' - trans = Maxval2CodeTrans() - assert trans.name == "Maxval2CodeTrans" + trans = Maxval2LoopTrans() + assert trans.name == "Maxval2LoopTrans" def test_validate(): @@ -131,10 +97,10 @@ def test_validate(): works as expected. ''' - trans = Maxval2CodeTrans() + trans = Maxval2LoopTrans() with pytest.raises(TransformationError) as info: trans.validate(None) - assert ("Error in Maxval2CodeTrans transformation. The supplied node " + assert ("Error in Maxval2LoopTrans transformation. The supplied node " "argument is not an intrinsic, found 'NoneType'." in str(info.value)) @@ -153,24 +119,23 @@ def test_apply(fortran_reader, fortran_writer, tmpdir): "end subroutine\n") expected = ( "subroutine maxval_test(array, n, m)\n" - " integer :: n\n integer :: m\n" + " integer :: n\n" + " integer :: m\n" " real, dimension(10,20) :: array\n" - " real :: result\n real :: maxval_var\n" - " integer :: i_0\n integer :: i_1\n\n" - " maxval_var = TINY(maxval_var)\n" - " do i_1 = 1, 20, 1\n" - " do i_0 = 1, 10, 1\n" - " if (maxval_var < array(i_0,i_1)) then\n" - " maxval_var = array(i_0,i_1)\n" - " end if\n" + " real :: result\n" + " integer :: idx\n" + " integer :: idx_1\n\n" + " result = TINY(result)\n" + " do idx = 1, 20, 1\n" + " do idx_1 = 1, 10, 1\n" + " result = MAX(result, array(idx_1,idx))\n" " enddo\n" - " enddo\n" - " result = maxval_var\n\n" + " enddo\n\n" "end subroutine maxval_test\n") psyir = fortran_reader.psyir_from_source(code) # FileContainer/Routine/Assignment/IntrinsicCall intrinsic_node = psyir.children[0].children[0].children[1] - trans = Maxval2CodeTrans() + trans = Maxval2LoopTrans() trans.apply(intrinsic_node) result = fortran_writer(psyir) assert result == expected diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/minval2code_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/minval2loop_trans_test.py similarity index 58% rename from src/psyclone/tests/psyir/transformations/intrinsics/minval2code_trans_test.py rename to src/psyclone/tests/psyir/transformations/intrinsics/minval2loop_trans_test.py index a90a101486..b21ecf4f8a 100644 --- a/src/psyclone/tests/psyir/transformations/intrinsics/minval2code_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/intrinsics/minval2loop_trans_test.py @@ -33,15 +33,14 @@ # ----------------------------------------------------------------------------- # Author: R. W. Ford, STFC Daresbury Laboratory -'''Module containing tests for the minval2code transformation.''' +'''Module containing tests for the minval2loop transformation.''' import pytest -from psyclone.psyir.nodes import Reference, ArrayReference -from psyclone.psyir.symbols import ( - REAL_TYPE, DataSymbol, INTEGER_TYPE, ArrayType) +from psyclone.psyir.nodes import Reference, Literal +from psyclone.psyir.symbols import REAL_TYPE, DataSymbol from psyclone.psyir.transformations import ( - Minval2CodeTrans, TransformationError) + Minval2LoopTrans, TransformationError) from psyclone.tests.utilities import Compile @@ -50,56 +49,23 @@ def test_initialise(): _INTRINSIC_NAME is set up as expected. ''' - trans = Minval2CodeTrans() - assert isinstance(trans, Minval2CodeTrans) + trans = Minval2LoopTrans() + assert isinstance(trans, Minval2LoopTrans) assert trans._INTRINSIC_NAME == "MINVAL" def test_loop_body(): - '''Test that the _loop_body method works as expected, without an array - reduction. - - ''' - trans = Minval2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - array_iterators = [j_iterator, i_iterator] - var_symbol = DataSymbol("var", REAL_TYPE) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator)]) - result = trans._loop_body(False, array_iterators, var_symbol, array_ref) - assert result.debug_string() == ( - "if (var > array(i,j)) then\n" - " var = array(i,j)\n" - "end if\n") - - -def test_loop_body_reduction(): - '''Test that the _loop_body method works as expected, with an array - reduction. - - ''' - trans = Minval2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - k_iterator = DataSymbol("k", INTEGER_TYPE) - array_iterators = [i_iterator, k_iterator] - var_symbol = DataSymbol("var", ArrayType(REAL_TYPE, [10, 10])) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator), - Reference(k_iterator)]) - result = trans._loop_body(True, array_iterators, var_symbol, array_ref) - assert result.debug_string() == ( - "if (var(i,k) > array(i,j,k)) then\n" - " var(i,k) = array(i,j,k)\n" - "end if\n") + '''Test that the _loop_body method works as expected.''' + trans = Minval2LoopTrans() + lhs = Reference(DataSymbol("i", REAL_TYPE)) + rhs = Literal("1.0", REAL_TYPE) + result = trans._loop_body(lhs, rhs) + assert "MIN(i, 1.0)" in result.debug_string() def test_init_var(): '''Test that the _init_var method works as expected.''' - trans = Minval2CodeTrans() + trans = Minval2LoopTrans() var_symbol = DataSymbol("var", REAL_TYPE) result = trans._init_var(var_symbol) # As 'huge' is not yet part of an expression, the 'debug_string()' @@ -112,7 +78,7 @@ def test_str(): as expected. ''' - trans = Minval2CodeTrans() + trans = Minval2LoopTrans() assert str(trans) == ("Convert the PSyIR MINVAL intrinsic to equivalent " "PSyIR code.") @@ -122,8 +88,8 @@ def test_name(): as expected. ''' - trans = Minval2CodeTrans() - assert trans.name == "Minval2CodeTrans" + trans = Minval2LoopTrans() + assert trans.name == "Minval2LoopTrans" def test_validate(): @@ -131,10 +97,10 @@ def test_validate(): works as expected. ''' - trans = Minval2CodeTrans() + trans = Minval2LoopTrans() with pytest.raises(TransformationError) as info: trans.validate(None) - assert ("Error in Minval2CodeTrans transformation. The supplied node " + assert ("Error in Minval2LoopTrans transformation. The supplied node " "argument is not an intrinsic, found 'NoneType'." in str(info.value)) @@ -152,26 +118,17 @@ def test_apply(fortran_reader, fortran_writer, tmpdir): " result = minval(array)\n" "end subroutine\n") expected = ( - "subroutine minval_test(array, n, m)\n" - " integer :: n\n integer :: m\n" - " real, dimension(10,20) :: array\n" - " real :: result\n real :: minval_var\n" - " integer :: i_0\n integer :: i_1\n\n" - " minval_var = HUGE(minval_var)\n" - " do i_1 = 1, 20, 1\n" - " do i_0 = 1, 10, 1\n" - " if (minval_var > array(i_0,i_1)) then\n" - " minval_var = array(i_0,i_1)\n" - " end if\n" + " result = HUGE(result)\n" + " do idx = 1, 20, 1\n" + " do idx_1 = 1, 10, 1\n" + " result = MIN(result, array(idx_1,idx))\n" " enddo\n" - " enddo\n" - " result = minval_var\n\n" - "end subroutine minval_test\n") + " enddo\n") psyir = fortran_reader.psyir_from_source(code) # FileContainer/Routine/Assignment/IntrinsicCall intrinsic_node = psyir.children[0].children[0].children[1] - trans = Minval2CodeTrans() + trans = Minval2LoopTrans() trans.apply(intrinsic_node) result = fortran_writer(psyir) - assert result == expected + assert expected in result assert Compile(tmpdir).string_compiles(result) diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/mms_base_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/mms_base_trans_test.py deleted file mode 100644 index efececd265..0000000000 --- a/src/psyclone/tests/psyir/transformations/intrinsics/mms_base_trans_test.py +++ /dev/null @@ -1,763 +0,0 @@ -# ----------------------------------------------------------------------------- -# BSD 3-Clause License -# -# Copyright (c) 2023, 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. -# ----------------------------------------------------------------------------- -# Author: R. W. Ford, STFC Daresbury Laboratory - -'''Module containing tests for the mms_base_trans which is an abstract -parent class for the sum2code_trans, minval2code_trans and -maxval2code_trans transformations. - -''' -import pytest - -from psyclone.psyir.nodes import IntrinsicCall, Reference, Literal, Assignment -from psyclone.psyir.symbols import ( - Symbol, BOOLEAN_TYPE, INTEGER_TYPE, DataSymbol, REAL_TYPE) -from psyclone.psyir.transformations import TransformationError, Sum2CodeTrans -from psyclone.psyir.transformations.intrinsics.mms_base_trans import ( - MMSBaseTrans) -from psyclone.tests.utilities import Compile - - -class TestTrans(MMSBaseTrans): - '''Utility class to allow the abstract MMSBaseTrans to be tested.''' - - def _loop_body(self, _1, _2, _3, array_ref): - '''Minimal implementation of the abstract _loop_body method.''' - node = Assignment.create(array_ref, Literal("33.0", REAL_TYPE)) - return node - - def _init_var(self, _): - '''Minimal implementation of the abstract _init_var method.''' - node = Literal("99.0", REAL_TYPE) - return node - - -class NamedTestTrans(TestTrans): - '''Utility class to allow the abstract MMSBaseTrans to be tested. Sets - the internal intrinsic_name. Needs to use an existing intrinsic - for the tests to work. - - ''' - _INTRINSIC_NAME = "SUM" - - -def test_init_exception(): - '''Check that this class can't be created as it is abstract.''' - # pylint: disable=abstract-class-instantiated - with pytest.raises(TypeError) as info: - _ = MMSBaseTrans() - # Python >= 3.12 tweaks the error message to mention - # the lack of an implementation and to quote the method names. - # We split the check to accomodate for this. - assert ("Can't instantiate abstract class MMSBaseTrans with" - in str(info.value)) - assert ("abstract methods" in str(info.value)) - assert ("_init_var" in str(info.value)) - assert ("_loop_body" in str(info.value)) - - -def test_get_args(): - '''Check the _get_args static method works as expected.''' - # array - array_reference = Reference(Symbol("array")) - node = IntrinsicCall.create(IntrinsicCall.Intrinsic.SUM, [array_reference]) - result = MMSBaseTrans._get_args(node) - assert result == (array_reference, None, None) - - # array, mask, dim - mask_reference = Literal("true", BOOLEAN_TYPE) - dim_reference = Literal("1", INTEGER_TYPE) - node = IntrinsicCall.create(IntrinsicCall.Intrinsic.SUM, [ - array_reference.copy(), ("mask", mask_reference), - ("dim", dim_reference)]) - result = MMSBaseTrans._get_args(node) - assert result == (array_reference, dim_reference, mask_reference) - - -def test_str(): - ''' Check that the __str__ method behaves as expected. ''' - assert str(TestTrans()) == ("Convert the PSyIR None intrinsic to " - "equivalent PSyIR code.") - assert str(NamedTestTrans()) == ("Convert the PSyIR SUM intrinsic to " - "equivalent PSyIR code.") - - -# validate method - -def test_validate_node(): - '''Check that an incorrect node raises the expected exception.''' - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(None) - assert ("Error in NamedTestTrans transformation. The supplied node " - "argument is not an intrinsic, found 'NoneType'." - in str(info.value)) - - intrinsic = IntrinsicCall.create( - IntrinsicCall.Intrinsic.MINVAL, - [Reference(DataSymbol("array", REAL_TYPE))]) - with pytest.raises(TransformationError) as info: - trans.validate(intrinsic) - assert ("The supplied node argument is not a sum intrinsic, found " - "'MINVAL'." in str(info.value)) - - -def test_structure_error(fortran_reader): - '''Test that the transformation raises an exception if the array node - is part of a structure, as this is not currently supported. - - ''' - code = ( - "subroutine test(n,m)\n" - " integer :: n, m\n" - " type :: array_type\n" - " real :: array(10,10)\n" - " end type\n" - " type(array_type) :: ref\n" - " real :: result\n" - " integer :: dimension\n" - " result = sum(ref%array)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - node = psyir.children[0].children[0].children[1] - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("NamedTestTrans only supports arrays or plain references for " - "the first argument, but found 'StructureReference'." - in str(info.value)) - - -def test_indexed_array_error(fortran_reader): - '''Test that the transformation raises an exception if the array node - has a literal index, as this is invalid. - - ''' - code = ( - "subroutine test(array,n,m)\n" - " integer :: n, m\n" - " real :: array(10,10)\n" - " real :: result\n" - " integer :: dimension\n" - " result = sum(array(1,1))\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - node = psyir.children[0].children[0].children[1] - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("NamedTestTrans only supports arrays with array ranges, but " - "found a fixed dimension in 'array(1,1)'." in str(info.value)) - - -def test_non_constant_dim_value_binop(fortran_reader): - '''Test that the expected exception is raised if the literal value of - the dim arg can not be determined (as it is a binary - operation). Use the Sum2CodeTrans transformations (a subclass of - MMSBaseTrans), as it makes it easier to raise this exception. - - ''' - code = ( - "subroutine test(array,a,b)\n" - " integer :: a,b\n" - " real :: array(10)\n" - " real :: result\n" - " result = sum(array, dim=a+b)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - trans = Sum2CodeTrans() - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("Can't find the value of the 'dim' argument to the SUM " - "intrinsic. Expected it to be a literal or a reference to " - "a known constant value, but found 'a + b' which is type " - "'BinaryOperation'." in str(info.value)) - - -def test_non_constant_dim_value_ref(fortran_reader): - '''Test that the expected exception is raised if the literal value of - the dim arg can not be determined (as it references an unknown - value). Use the Sum2CodeTrans transformations (a subclass of - MMSBaseTrans), as it makes it easier to raise this exception. - - ''' - code = ( - "subroutine test(array)\n" - " use my_mod, only : y\n" - " integer :: x=y\n" - " real :: array(10)\n" - " real :: result\n" - " result = sum(array, dim=x)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - trans = Sum2CodeTrans() - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("Can't find the value of the 'dim' argument to the SUM " - "intrinsic. Expected it to be a literal or a reference to " - "a known constant value, but found 'x' which is a reference " - "to a 'DataSymbol'." in str(info.value)) - - -def test_lhs(fortran_reader, fortran_writer, tmpdir): - '''Test that the correct code is produced when the minval, maxval or - sum is on the LHS of an asssignment. Uses the Sum2CodeTrans - transformation (a subclass of MMSBaseTrans), as it is easier to - test. - - ''' - code = ( - "subroutine test(array)\n" - " real :: array(10)\n" - " real :: result(10)\n" - " result(sum(array)) = 0.0\n" - "end subroutine\n") - expected = ( - "subroutine test(array)\n" - " real, dimension(10) :: array\n" - " real, dimension(10) :: result\n" - " real :: sum_var\n" - " integer :: i_0\n\n" - " sum_var = 0.0\n" - " do i_0 = 1, 10, 1\n" - " sum_var = sum_var + array(i_0)\n" - " enddo\n" - " result(sum_var) = 0.0\n\n" - "end subroutine test\n") - - psyir = fortran_reader.psyir_from_source(code) - trans = Sum2CodeTrans() - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[0].children[0] - trans.apply(node) - result = fortran_writer(psyir) - assert result == expected - assert Compile(tmpdir).string_compiles(result) - - -def test_array_arg(fortran_reader): - '''Test that the expected exception is raised if the array argument is - not an array. - - ''' - code = ( - "subroutine test(array,n,m)\n" - " integer :: n, m\n" - " real :: array\n" - " real :: result\n" - " result = sum(array)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert "Expected 'array' to be an array." in str(info.value) - - -def test_array_shape(fortran_reader, monkeypatch): - '''Tests that the expected exception is raised if the array range is - not a valid value. Requires monkeypatching. - - ''' - code = ( - "subroutine test(array,n,m)\n" - " integer :: n, m\n" - " real :: array(1)\n" - " real :: result\n" - " result = sum(array)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - - # Modify array shape from node to create exception - array_ref = node.children[0] - array_symbol = array_ref.symbol - monkeypatch.setattr(array_symbol._datatype, "_shape", [None]) - - trans = NamedTestTrans() - with pytest.raises(TypeError) as info: - trans.validate(node) - assert ("ArrayType shape-list elements can only be 'int', " - "ArrayType.Extent, 'DataNode' or a 2-tuple thereof but found " - "'NoneType'." in str(info.value)) - - -def test_unexpected_shape(fortran_reader, monkeypatch): - '''Tests that the expected exception is raised if the array shape is - not a valid value. Requires monkeypatching. - - ''' - code = ( - "subroutine test(array,n,m)\n" - " integer :: n, m\n" - " real :: array(1)\n" - " real :: result\n" - " result = sum(array)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - array_ref = node.children[0] - # Modify the shape of the array reference shape to create an - # exception - monkeypatch.setattr(array_ref.symbol._datatype, "_shape", [1]) - - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("Unexpected shape for array. Expecting one of Deferred, Attribute " - "or Bounds but found '1'." in str(info.value)) - - -def test_array_type_arg(fortran_reader): - '''Test that the expected exception is raised if the array is an - unsupported datatype. - - ''' - code = ( - "subroutine test(array,n,m)\n" - " integer :: n, m\n" - " logical :: array(10)\n" - " real :: result\n" - " result = sum(array)\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.children[0].children[0].children[1] - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("Only real and integer types supported for array 'array', " - "but found 'BOOLEAN'." in str(info.value)) - - -def test_not_assignment(fortran_reader): - '''Test that the expected exception is raised if the intrinsic call is - not part of an assignment (e.g. is an argument to a subroutine), - as this is not currently supported. - - ''' - code = ( - "subroutine test(array)\n" - " integer :: array(10)\n" - " call routine(sum(array))\n" - "end subroutine\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Call/IntrinsicCall - node = psyir.children[0].children[0].children[0] - trans = NamedTestTrans() - with pytest.raises(TransformationError) as info: - trans.validate(node) - assert ("NamedTestTrans only works when the intrinsic is part " - "of an Assignment" in str(info.value)) - - -# apply - -@pytest.mark.parametrize("idim1,idim2,rdim11,rdim12,rdim21,rdim22", - [("10", "20", "1", "10", "1", "20"), - ("n", "m", "1", "n", "1", "m"), - ("0:n", "2:m", "0", "n", "2", "m"), - (":", ":", "LBOUND(array, dim=1)", - "UBOUND(array, dim=1)", - "LBOUND(array, dim=2)", - "UBOUND(array, dim=2)")]) -def test_apply(idim1, idim2, rdim11, rdim12, rdim21, rdim22, - fortran_reader, fortran_writer, tmpdir): - '''Test that a sum intrinsic as the only term on the rhs of an - assignment with a single array argument gets transformed as - expected. Test with known and unknown array sizes. What we care - about here are the sum_var array and the generated loop bounds. - - ''' - code = ( - f"subroutine test(array,n,m)\n" - f" integer :: n, m\n" - f" real :: array({idim1},{idim2})\n" - f" real :: result\n" - f" result = sum(array)\n" - f"end subroutine\n") - expected_decl = " real :: sum_var\n" - expected_bounds = ( - f" do i_1 = {rdim21}, {rdim22}, 1\n" - f" do i_0 = {rdim11}, {rdim12}, 1\n") - expected_result = " result = sum_var\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_apply_multi(fortran_reader, fortran_writer, tmpdir): - '''Test that a sum intrinsic as part of multiple term on the rhs of an - assignment with a single array argument gets transformed as - expected. What we care about here are the sum_var array and the - generated loop bounds. - - ''' - code = ( - "subroutine test(array,n,m,value1,value2)\n" - " integer :: n, m\n" - " real :: array(n,m)\n" - " real :: value1, value2\n" - " real :: result\n" - " result = value1 + sum(array) * value2\n" - "end subroutine\n") - expected_decl = " real :: sum_var\n" - expected_bounds = ( - " do i_1 = 1, m, 1\n" - " do i_0 = 1, n, 1\n") - expected_result = " result = value1 + sum_var * value2\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ - # BinaryOperation(MUL)/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_apply_dimension_1d(fortran_reader, fortran_writer, tmpdir): - '''Test that the apply method works as expected when a dimension - argument is specified and the array is one dimensional. This - should be the same as if dimension were not specified at all. - What we care about here are the sum_var array and the generated - loop bounds. - - ''' - code = ( - "subroutine test(array,value1,value2)\n" - " real :: array(:)\n" - " real :: value1, value2\n" - " real :: result\n" - " result = value1 + sum(array,dim=1) * value2\n" - "end subroutine\n") - expected_decl = " real :: sum_var\n" - expected_bounds = " do i_0 = LBOUND(array, dim=1), " - expected_bounds += "UBOUND(array, dim=1), 1\n" - expected_result = " result = value1 + sum_var * value2\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ - # BinaryOperation(MUL)/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_apply_dimension_multid(fortran_reader, fortran_writer, tmpdir): - '''Test that the apply method works as expected when a dimension - argument is specified and the array is multi-dimensional. What we - care about here are the sum_var array and the generated loop - bounds. - - ''' - code = ( - "subroutine test(array,value1,value2,n,m,p)\n" - " integer :: n,m,p\n" - " real :: array(n,m,p)\n" - " real :: value1, value2\n" - " real :: result(n,p)\n" - " result(:,:) = value1 + sum(array,dim=2) * value2\n" - "end subroutine\n") - expected_decl = " real, dimension(n,p) :: sum_var\n" - expected_bounds = ( - " do i_2 = 1, p, 1\n" - " do i_1 = 1, m, 1\n" - " do i_0 = 1, n, 1\n") - expected_result = " result(:,:) = value1 + sum_var(:,:) * value2\n\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ - # BinaryOperation(MUL)/IntrinsicCall - node = [intr for intr in psyir.walk(IntrinsicCall) - if intr.intrinsic == IntrinsicCall.Intrinsic.SUM][0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_apply_dimension_multid_unknown( - fortran_reader, fortran_writer, tmpdir): - '''Test that lbound and ubound are used to declare the sum_var - variable and for the loop bounds if the bounds of the array are - not known. - - ''' - code = ( - "subroutine test(array,value1,value2,result)\n" - " real :: array(:,:,:)\n" - " real :: value1, value2\n" - " real :: result(:,:)\n" - " result(:,:) = value1 + sum(array,dim=2) * value2\n" - "end subroutine\n") - expected_decl = ( - " real, dimension(LBOUND(array, dim=1):UBOUND(array, dim=1)," - "LBOUND(array, dim=3):UBOUND(array, dim=3)) :: sum_var\n") - expected_bounds = ( - " do i_2 = LBOUND(array, dim=3), UBOUND(array, dim=3), 1\n" - " do i_1 = LBOUND(array, dim=2), UBOUND(array, dim=2), 1\n" - " do i_0 = LBOUND(array, dim=1), UBOUND(array, dim=1), 1\n") - expected_result = " result(:,:) = value1 + sum_var(:,:) * value2\n\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ - # BinaryOperation(MUL)/IntrinsicCall - node = [intr for intr in psyir.walk(IntrinsicCall) - if intr.intrinsic == IntrinsicCall.Intrinsic.SUM][0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_apply_dimension_multid_range(fortran_reader, fortran_writer, tmpdir): - '''Test that the apply method works as expected when an array range is - specified and the array is multi-dimensional. What we - care about here are the sum_var array and the generated loop - bounds. - - ''' - code = ( - "subroutine test(array,value1,value2,n,m,p)\n" - " integer :: n,m,p\n" - " real :: array(:,:,:)\n" - " real :: value1, value2\n" - " real :: result(n,p)\n" - " result(:,:) = value1 + sum(array(1:n,m-1:m,1:p),dim=2) * " - "value2\n" - "end subroutine\n") - expected_decl = " real, dimension(n,p) :: sum_var\n" - expected_bounds = ( - " do i_2 = 1, p, 1\n" - " do i_1 = m - 1, m, 1\n" - " do i_0 = 1, n, 1\n") - expected_result = " result(:,:) = value1 + sum_var(:,:) * value2\n\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/BinaryOperation(ADD)/ - # BinaryOperation(MUL)/IntrinsicCall - node = [intr for intr in psyir.walk(IntrinsicCall) - if intr.intrinsic == IntrinsicCall.Intrinsic.SUM][0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_mask(fortran_reader, fortran_writer, tmpdir): - '''Test that the transformation works when there is a mask specified. - What we care about here are the sum_var array, the generated loop - bounds and the mask. - - ''' - code = ( - "program test\n" - " real :: array(10,10)\n" - " real :: result\n" - " result = sum(array, mask=MOD(array, 2.0)==1)\n" - "end program\n") - # We are using the NamedTestTrans subclass here which simply sets - # the value of the array to 33. - expected = ( - " do i_1 = 1, 10, 1\n" - " do i_0 = 1, 10, 1\n" - " if (MOD(array(i_0,i_1), 2.0) == 1) then\n" - " array(i_0,i_1) = 33.0\n" - " end if\n" - " enddo\n" - " enddo\n") - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected in result - assert Compile(tmpdir).string_compiles(result) - - -def test_mask_dimension(fortran_reader, fortran_writer, tmpdir): - '''Test that the transformation works when there is a mask and a - dimension specified. What we care about here are the sum_var - array, the generated loop bounds and the mask. - - ''' - code = ( - "program test\n" - " real :: array(10,10)\n" - " real :: result(10)\n" - " integer, parameter :: dimension=2\n" - " result = sum(array, dimension, mask=MOD(array, 2.0)==1)\n" - "end program\n") - expected_decl = " real, dimension(10) :: sum_var\n" - # We are using the NamedTestTrans subclass here which simply sets - # the value of the array to 33. - expected_bounds = ( - " do i_1 = 1, 10, 1\n" - " do i_0 = 1, 10, 1\n" - " if (MOD(array(i_0,i_1), 2.0) == 1) then\n" - " array(i_0,i_1) = 33.0\n" - " end if\n") - expected_result = " result = sum_var(:)\n" - psyir = fortran_reader.psyir_from_source(code) - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans = NamedTestTrans() - trans.apply(node) - result = fortran_writer(psyir) - assert expected_decl in result - assert expected_bounds in result - assert expected_result in result - assert Compile(tmpdir).string_compiles(result) - - -def test_mask_array_indexed(fortran_reader, fortran_writer, tmpdir): - '''Test that the mask code works if the array iself it used as part of - the mask. In this case it will already be indexed. Use the - Sum2CodeTrans transformation (a subclass of MMSBaseTrans), as it - is easier to test. - - ''' - code = ( - "program sum_test\n" - " integer :: a(4)\n" - " integer :: result\n" - " a(1) = 2\n" - " a(2) = 1\n" - " a(3) = 2\n" - " a(4) = 1\n" - " result = sum(a, mask=a(1)>a)\n" - "end program\n") - expected = ( - "program sum_test\n" - " integer, dimension(4) :: a\n" - " integer :: result\n" - " integer :: sum_var\n" - " integer :: i_0\n\n" - " a(1) = 2\n" - " a(2) = 1\n" - " a(3) = 2\n" - " a(4) = 1\n" - " sum_var = 0\n" - " do i_0 = 1, 4, 1\n" - " if (a(1) > a(i_0)) then\n" - " sum_var = sum_var + a(i_0)\n" - " end if\n" - " enddo\n" - " result = sum_var\n\n" - "end program sum_test\n") - psyir = fortran_reader.psyir_from_source(code) - trans = Sum2CodeTrans() - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.walk(IntrinsicCall)[0] - trans.apply(node) - result = fortran_writer(psyir) - assert result == expected - assert Compile(tmpdir).string_compiles(result) - - -def test_allocate_dim(fortran_reader, fortran_writer, tmpdir): - '''Test that a newly created array is allocated after the original - array is allocated (if the original array is allocated). Use the - Sum2CodeTrans transformations (a subclass of MMSBaseTrans), as it - is easier to test. - - ''' - code = ( - "program sum_test\n" - " integer, allocatable :: a(:,:,:)\n" - " integer :: result(4,4)\n" - " allocate(a(4,4,4))\n" - " result = sum(a, dim=2)\n" - " deallocate(a)\n" - "end program\n") - expected = ( - "program sum_test\n" - " integer, allocatable, dimension(:,:,:) :: a\n" - " integer, dimension(4,4) :: result\n" - " integer, allocatable, dimension(:,:) :: sum_var\n" - " integer :: i_0\n" - " integer :: i_1\n" - " integer :: i_2\n\n" - " ALLOCATE(a(1:4,1:4,1:4))\n" - " ALLOCATE(sum_var(LBOUND(a, dim=1):UBOUND(a, dim=1)," - "LBOUND(a, dim=3):UBOUND(a, dim=3)))\n" - " sum_var(:,:) = 0\n" - " do i_2 = LBOUND(a, dim=3), UBOUND(a, dim=3), 1\n" - " do i_1 = LBOUND(a, dim=2), UBOUND(a, dim=2), 1\n" - " do i_0 = LBOUND(a, dim=1), UBOUND(a, dim=1), 1\n" - " sum_var(i_0,i_2) = sum_var(i_0,i_2) + a(i_0,i_1,i_2)\n" - " enddo\n" - " enddo\n" - " enddo\n" - " result = sum_var(:,:)\n" - " DEALLOCATE(a)\n\n" - "end program sum_test\n") - psyir = fortran_reader.psyir_from_source(code) - trans = Sum2CodeTrans() - # FileContainer/Routine/Assignment/IntrinsicCall - node = psyir.walk(IntrinsicCall)[1] - trans.apply(node) - result = fortran_writer(psyir) - assert result == expected - assert Compile(tmpdir).string_compiles(result) diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/product2loop_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/product2loop_trans_test.py new file mode 100644 index 0000000000..18060bb6ea --- /dev/null +++ b/src/psyclone/tests/psyir/transformations/intrinsics/product2loop_trans_test.py @@ -0,0 +1,142 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2023, 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. +# ----------------------------------------------------------------------------- +# Author: R. W. Ford, STFC Daresbury Laboratory + +'''Module containing tests for the product2loop transformation.''' + +import pytest + +from psyclone.psyir.nodes import Reference, Literal +from psyclone.psyir.symbols import ( + REAL_TYPE, DataSymbol, INTEGER_TYPE, ScalarType) +from psyclone.psyir.transformations import ( + Product2LoopTrans, TransformationError) +from psyclone.tests.utilities import Compile + + +def test_initialise(): + '''Test that we can create an instance of the transformation and that + _INTRINSIC_NAME is set up as expected. + + ''' + trans = Product2LoopTrans() + assert isinstance(trans, Product2LoopTrans) + assert trans._INTRINSIC_NAME == "PRODUCT" + + +def test_loop_body(): + '''Test that the _loop_body method works as expected.''' + trans = Product2LoopTrans() + lhs = Reference(DataSymbol("i", REAL_TYPE)) + rhs = Literal("1.0", REAL_TYPE) + result = trans._loop_body(lhs, rhs) + assert "i * 1.0" in result.debug_string() + + +@pytest.mark.parametrize("name,precision,one", [ + (ScalarType.Intrinsic.REAL, ScalarType.Precision.UNDEFINED, "1.0"), + (ScalarType.Intrinsic.INTEGER, ScalarType.Precision.UNDEFINED, "1"), + (ScalarType.Intrinsic.REAL, DataSymbol("r_def", INTEGER_TYPE), + "1.0_r_def")]) +def test_init_var(name, precision, one): + '''Test that the _init_var method works as expected. Test with real, + integer and with a specified precision. + + ''' + trans = Product2LoopTrans() + datatype = ScalarType(name, precision) + var_symbol = DataSymbol("var", datatype) + result = trans._init_var(var_symbol) + assert result.debug_string() == one + + +def test_str(): + '''Test that the str method, implemented in the parent class, works + as expected. + + ''' + trans = Product2LoopTrans() + assert str(trans) == ("Convert the PSyIR PRODUCT intrinsic to equivalent " + "PSyIR code.") + + +def test_name(): + '''Test that the name method, implemented in the parent class, works + as expected. + + ''' + trans = Product2LoopTrans() + assert trans.name == "Product2LoopTrans" + + +def test_validate(): + '''Test that the validate method, implemented in the parent class, + works as expected. + + ''' + trans = Product2LoopTrans() + with pytest.raises(TransformationError) as info: + trans.validate(None) + assert ("Error in Product2LoopTrans transformation. The supplied node " + "argument is not an intrinsic, found 'NoneType'." + in str(info.value)) + + +def test_apply(fortran_reader, fortran_writer, tmpdir): + '''Test that the apply method, implemented in the parent class, works + as expected. + + ''' + code = ( + "subroutine product_test(array,n,m)\n" + " integer :: n, m\n" + " real :: array(10,20)\n" + " real :: result\n" + " result = product(array)\n" + "end subroutine\n") + expected = ( + " result = 1.0\n" + " do idx = 1, 20, 1\n" + " do idx_1 = 1, 10, 1\n" + " result = result * array(idx_1,idx)\n" + " enddo\n" + " enddo\n") + psyir = fortran_reader.psyir_from_source(code) + # FileContainer/Routine/Assignment/IntrinsicCall + intrinsic_node = psyir.children[0].children[0].children[1] + trans = Product2LoopTrans() + trans.apply(intrinsic_node) + result = fortran_writer(psyir) + assert expected in result + assert Compile(tmpdir).string_compiles(result) diff --git a/src/psyclone/tests/psyir/transformations/intrinsics/sum2code_trans_test.py b/src/psyclone/tests/psyir/transformations/intrinsics/sum2loop_trans_test.py similarity index 62% rename from src/psyclone/tests/psyir/transformations/intrinsics/sum2code_trans_test.py rename to src/psyclone/tests/psyir/transformations/intrinsics/sum2loop_trans_test.py index 5420951cf5..af9ceef74a 100644 --- a/src/psyclone/tests/psyir/transformations/intrinsics/sum2code_trans_test.py +++ b/src/psyclone/tests/psyir/transformations/intrinsics/sum2loop_trans_test.py @@ -33,14 +33,14 @@ # ----------------------------------------------------------------------------- # Author: R. W. Ford, STFC Daresbury Laboratory -'''Module containing tests for the sum2code transformation.''' +'''Module containing tests for the sum2loop transformation.''' import pytest -from psyclone.psyir.nodes import Reference, ArrayReference +from psyclone.psyir.nodes import Reference, Literal from psyclone.psyir.symbols import ( - REAL_TYPE, DataSymbol, INTEGER_TYPE, ArrayType, ScalarType) -from psyclone.psyir.transformations import Sum2CodeTrans, TransformationError + REAL_TYPE, DataSymbol, INTEGER_TYPE, ScalarType) +from psyclone.psyir.transformations import Sum2LoopTrans, TransformationError from psyclone.tests.utilities import Compile @@ -49,45 +49,18 @@ def test_initialise(): _INTRINSIC_NAME is set up as expected. ''' - trans = Sum2CodeTrans() - assert isinstance(trans, Sum2CodeTrans) + trans = Sum2LoopTrans() + assert isinstance(trans, Sum2LoopTrans) assert trans._INTRINSIC_NAME == "SUM" def test_loop_body(): - '''Test that the _loop_body method works as expected, without an array - reduction. - - ''' - trans = Sum2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - array_iterators = [j_iterator, i_iterator] - var_symbol = DataSymbol("var", REAL_TYPE) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator)]) - result = trans._loop_body(False, array_iterators, var_symbol, array_ref) - assert result.debug_string() == "var = var + array(i,j)\n" - - -def test_loop_body_reduction(): - '''Test that the _loop_body method works as expected, with an array - reduction. - - ''' - trans = Sum2CodeTrans() - i_iterator = DataSymbol("i", INTEGER_TYPE) - j_iterator = DataSymbol("j", INTEGER_TYPE) - k_iterator = DataSymbol("k", INTEGER_TYPE) - array_iterators = [i_iterator, k_iterator] - var_symbol = DataSymbol("var", ArrayType(REAL_TYPE, [10, 10])) - array_symbol = DataSymbol("array", ArrayType(REAL_TYPE, [10, 10, 10])) - array_ref = ArrayReference.create( - array_symbol, [Reference(i_iterator), Reference(j_iterator), - Reference(k_iterator)]) - result = trans._loop_body(True, array_iterators, var_symbol, array_ref) - assert result.debug_string() == "var(i,k) = var(i,k) + array(i,j,k)\n" + '''Test that the _loop_body method works as expected.''' + trans = Sum2LoopTrans() + lhs = Reference(DataSymbol("i", REAL_TYPE)) + rhs = Literal("1.0", REAL_TYPE) + result = trans._loop_body(lhs, rhs) + assert "i + 1.0" in result.debug_string() @pytest.mark.parametrize("name,precision,zero", [ @@ -100,7 +73,7 @@ def test_init_var(name, precision, zero): integer and with a specified precision. ''' - trans = Sum2CodeTrans() + trans = Sum2LoopTrans() datatype = ScalarType(name, precision) var_symbol = DataSymbol("var", datatype) result = trans._init_var(var_symbol) @@ -112,7 +85,7 @@ def test_str(): as expected. ''' - trans = Sum2CodeTrans() + trans = Sum2LoopTrans() assert str(trans) == ("Convert the PSyIR SUM intrinsic to equivalent " "PSyIR code.") @@ -122,8 +95,8 @@ def test_name(): as expected. ''' - trans = Sum2CodeTrans() - assert trans.name == "Sum2CodeTrans" + trans = Sum2LoopTrans() + assert trans.name == "Sum2LoopTrans" def test_validate(): @@ -131,10 +104,10 @@ def test_validate(): works as expected. ''' - trans = Sum2CodeTrans() + trans = Sum2LoopTrans() with pytest.raises(TransformationError) as info: trans.validate(None) - assert ("Error in Sum2CodeTrans transformation. The supplied node " + assert ("Error in Sum2LoopTrans transformation. The supplied node " "argument is not an intrinsic, found 'NoneType'." in str(info.value)) @@ -152,24 +125,17 @@ def test_apply(fortran_reader, fortran_writer, tmpdir): " result = sum(array)\n" "end subroutine\n") expected = ( - "subroutine sum_test(array, n, m)\n" - " integer :: n\n integer :: m\n" - " real, dimension(10,20) :: array\n" - " real :: result\n real :: sum_var\n" - " integer :: i_0\n integer :: i_1\n\n" - " sum_var = 0.0\n" - " do i_1 = 1, 20, 1\n" - " do i_0 = 1, 10, 1\n" - " sum_var = sum_var + array(i_0,i_1)\n" + " result = 0.0\n" + " do idx = 1, 20, 1\n" + " do idx_1 = 1, 10, 1\n" + " result = result + array(idx_1,idx)\n" " enddo\n" - " enddo\n" - " result = sum_var\n\n" - "end subroutine sum_test\n") + " enddo\n") psyir = fortran_reader.psyir_from_source(code) # FileContainer/Routine/Assignment/IntrinsicCall intrinsic_node = psyir.children[0].children[0].children[1] - trans = Sum2CodeTrans() + trans = Sum2LoopTrans() trans.apply(intrinsic_node) result = fortran_writer(psyir) - assert result == expected + assert expected in result assert Compile(tmpdir).string_compiles(result)