Source code for psyclone.psyir.transformations.intrinsics.array_reduction_base_trans

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

'''Module providing common functionality to transformation from a
PSyIR array-reduction intrinsic to PSyIR code.

'''
from abc import ABC, abstractmethod
import warnings

from psyclone.psyir.nodes import (
    Assignment, Reference, ArrayReference, IfBlock, IntrinsicCall, Node,
    UnaryOperation, BinaryOperation)
from psyclone.psyir.symbols import (
    ArrayType, DataSymbol, UnresolvedType, UnsupportedType)
from psyclone.psyGen import Transformation
from psyclone.psyir.transformations.reference2arrayrange_trans import \
    Reference2ArrayRangeTrans
from psyclone.psyir.transformations.transformation_error import \
    TransformationError
from psyclone.utils import transformation_documentation_wrapper
from psyclone.psyir.transformations import ArrayAssignment2LoopsTrans


[docs] @transformation_documentation_wrapper 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 def __str__(self): return (f"Convert the PSyIR {self._INTRINSIC_NAME} intrinsic " "to equivalent PSyIR code.") # pylint: disable=too-many-branches
[docs] def validate(self, node, options=None, **kwargs): '''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. ''' super().validate(node, options=options, **kwargs) if not options: self.validate_options(**kwargs) 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}'.") try: node.compute_argument_names() except (ValueError, NotImplementedError) as err: raise TransformationError( f"Error in {self.name} transformation. Cannot " f"disambiguate the arguments names in '{node.debug_string()}'" ) from err array_ref = node.argument_by_name("array") dim_ref = node.argument_by_name("dim") if dim_ref: raise TransformationError( f"The dimension argument to {self._INTRINSIC_NAME} is not " f"yet supported.") if isinstance(node.datatype, (UnresolvedType, UnsupportedType)): raise TransformationError( f"Error in {self.name} transformation. Cannot create " f"a temporary variable for '{node.debug_string()}' " f"because it is of '{node.datatype}'.") # 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
[docs] def apply(self, node, options=None, verbose: bool = False, **kwargs): '''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]] :raises TransformationError: if node only contains Reference nodes, and they can't be convered to ArrayReferences by the calls to Reference2ArrayRangeTrans. ''' # TODO 2668: options are now deprecated: if options: warnings.warn(self._deprecation_warning, DeprecationWarning, 2) verbose = options.get("verbose", False) self.validate(node, options, verbose=verbose, **kwargs) # Get nodes of interest orig_assignment = node.ancestor(Assignment) array_arg = node.argument_by_name("array") mask_arg = node.argument_by_name("mask") symtab = node.scope.symbol_table # Step 1: Convert all references to arrays within the intrinsic array # and mask arguments (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. # We need to do this in a detached copy because Reference2ArrayRange # does not normally expand arguments of non-elemental functions. dummy = UnaryOperation.create(UnaryOperation.Operator.PLUS, array_arg.copy()) for reference in dummy.walk(Reference): Reference2ArrayRangeTrans().apply(reference) new_array_expr = dummy.children[0] if mask_arg: dummy = UnaryOperation.create(UnaryOperation.Operator.PLUS, mask_arg.copy()) for reference in dummy.walk(Reference): Reference2ArrayRangeTrans().apply(reference) new_mask_expr = dummy.children[0] # Step 2: Put the intrinsic's extracted expression on the rhs of an # assignment with one of the arrays within the expression being added # to the lhs of the assignment (because ArrayAssignment2Loops uses it # to obtain the loop bounds). For example if: # x = maxval(a(:,:)+b(:,:)) # then # rhs = a(:,:)+b(:,:) # resulting in the following code being created: # a(:,:) = a(:,:)+b(:,:) array_refs = new_array_expr.walk(ArrayReference) if len(array_refs) == 0: raise TransformationError( f"Can't apply {self.name} to {node.debug_string()} due " f"to no ArrayReference nodes present." ) assignment = Assignment.create(array_refs[0].copy(), new_array_expr.detach()) # If there is a mask, also add its expression to the assignment if mask_arg: combined_expression = BinaryOperation.create( BinaryOperation.Operator.AND, assignment.rhs.detach(), new_mask_expr.detach()) assignment.addchild(combined_expression) # Replace the existing expression so the new code gets replaces by # loops in-place and the new iteration symbols have access to the scope orig_assignment.replace_with(assignment) # Step 3: call ArrayAssignment2Loops to create loop and array indexing # to the array ranges created in step 2. 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 assignment_parent = assignment.parent assignment_position = assignment.position # Must be placed here to avoid circular imports # pylint: disable=import-outside-toplevel try: ArrayAssignment2LoopsTrans().apply(assignment) except TransformationError as err: # The ArrayAssignment2LoopsTrans could fail to convert the ranges, # unfortunately this can not be tested before modifications to the # tree (e.g. in the validate), so the best we can do is reverting # to the original statement (with maybe some leftover tmp variable) # and produce the error here. assignment.replace_with(orig_assignment) if verbose: orig_assignment.append_preceding_comment( f"{self.name} failed because ArrayAssignment2LoopsTrans: " f"{err.value}" ) # pylint: disable=raise-missing-from raise TransformationError( f"ArrayAssignment2LoopsTrans could not convert the " f"expression:\n{assignment.debug_string()}\n into a loop " f"because:\n{err.value}") outer_loop = assignment_parent.children[assignment_position] if mask_arg: # remove mask from the rhs of the assignment indexed_mask_expr = combined_expression.children[1].detach() assignment.rhs.replace_with( combined_expression.children[0].detach() ) # Step 4: convert the original assignment (now within a loop # and indices) to its intrinsic form by replacing the # assignment with tmp=INTRINSIC(orig_lhs,<expr>). 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 # Create a temporary symbol to accumulate the reduction value (make # sure to do that after ArrayAssignment2LoopsTrans, so it does not # have to be cleaned up if that transformation fails) tmp_symbol = symtab.new_symbol( root_name="reduction_var", symbol_type=DataSymbol, datatype=node.datatype) tmp_ref = Reference(tmp_symbol) new_assignment = Assignment.create( tmp_ref.copy(), self._loop_body( tmp_ref.copy(), assignment.rhs.copy())) if mask_arg: # Place the indexed mask around the statement. new_assignment = IfBlock.create( indexed_mask_expr.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 # Initialisation statement: lhs = tmp_ref.copy() rhs = self._init_var(lhs) assignment = Assignment.create(lhs, rhs) if verbose: code = orig_assignment.debug_string().strip() assignment.append_preceding_comment( f"{self.name} expansion of: {code}") outer_loop.parent.children.insert(outer_loop.position, assignment) # Update original assignment with the reduced value (after that # the orig_assignment will not be usable) replacement_assignment = orig_assignment for child in replacement_assignment.walk(Node): if child is node: child.replace_with(tmp_ref.copy()) break outer_loop.parent.children.insert(outer_loop.position+1, replacement_assignment)
@abstractmethod def _loop_body(self, lhs, rhs): '''The intrinsic-specific content of the created loop body.''' @abstractmethod def _init_var(self, reference): '''The intrinsic-specific initial value for the temporary variable. :param reference: the reference used to store the final result. :type reference: :py:class:`psyclone.psyir.node.Reference` '''
# For AutoAPI auto-documentation generation. __all__ = ["ArrayReductionBaseTrans"]