# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2022-2025, 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 a transformation from a PSyIR DOT_PRODUCT operator
to PSyIR code. This could be useful if the DOT_PRODUCT operator is not
supported by the back-end or if the performance in the inline code is
better than the intrinsic.
'''
# pylint: disable=too-many-locals
from psyclone.psyir.nodes import BinaryOperation, Assignment, Reference, \
Loop, Literal, ArrayReference, Range, Routine, IntrinsicCall
from psyclone.psyir.symbols import (DataSymbol, INTEGER_TYPE, REAL_TYPE,
ScalarType)
from psyclone.psyir.transformations.transformation_error \
import TransformationError
from psyclone.psyir.transformations.intrinsics.intrinsic2code_trans import \
Intrinsic2CodeTrans
from psyclone.utils import transformation_documentation_wrapper
[docs]
@transformation_documentation_wrapper
class DotProduct2CodeTrans(Intrinsic2CodeTrans):
'''Provides a transformation from a PSyIR DOT_PRODUCT Operator node to
equivalent code in a PSyIR tree. Validity checks are also
performed.
If ``R`` is a scalar and ``A``, and ``B`` have dimension ``N``,
The transformation replaces:
.. code-block:: fortran
R = ... DOT_PRODUCT(A,B) ...
with the following code:
.. code-block:: fortran
TMP = 0.0
do I=1,N
TMP = TMP + A(i)*B(i)
R = ... TMP ...
For example:
>>> from psyclone.psyir.backend.fortran import FortranWriter
>>> from psyclone.psyir.frontend.fortran import FortranReader
>>> from psyclone.psyir.nodes import IntrinsicCall
>>> from psyclone.psyir.transformations import DotProduct2CodeTrans
>>> code = ("subroutine dot_product_test(v1,v2)\\n"
... "real,intent(in) :: v1(10), v2(10)\\n"
... "real :: result\\n"
... "result = dot_product(v1,v2)\\n"
... "end subroutine\\n")
>>> psyir = FortranReader().psyir_from_source(code)
>>> trans = DotProduct2CodeTrans()
>>> trans.apply(psyir.walk(IntrinsicCall)[0])
>>> print(FortranWriter()(psyir))
subroutine dot_product_test(v1, v2)
real, dimension(10), intent(in) :: v1
real, dimension(10), intent(in) :: v2
real :: result
integer :: i
real :: res_dot_product
<BLANKLINE>
res_dot_product = 0.0
do i = 1, 10, 1
res_dot_product = res_dot_product + v1(i) * v2(i)
enddo
result = res_dot_product
<BLANKLINE>
end subroutine dot_product_test
<BLANKLINE>
'''
def __init__(self):
super().__init__()
self._intrinsic = IntrinsicCall.Intrinsic.DOT_PRODUCT
[docs]
def validate(self, node, options=None, **kwargs):
'''Perform checks to ensure that it is valid to apply the
DotProduct2CodeTran transformation to the supplied node.
Note, this validation does not check for invalid argument
combinations to dot_product (e.g. different precision or
different datatypes) as that should have already been picked
up when creating the PSyIR.
:param node: the node that is being checked.
:type node: :py:class:`psyclone.psyir.nodes.BinaryOperation`
:param options: a dictionary with options for transformations.
:type options: dict of str:str or None
:raises TransformationError: if one of the arguments is not a \
Reference node.
:raises TransformationError: if an argument does not use array \
slice notation and is not a 1d array.
:raises TransformationError: if an argument uses array slice \
notation but the array slice is not for the first dimension of \
the array.
:raises TransformationError: if an argument uses array slice \
notation but it is not for the full range of the dimension.
'''
super().validate(node, options, **kwargs)
# Both arguments should be references (or array references)
for arg in node.children:
if arg.__class__ not in [Reference, ArrayReference]:
raise TransformationError(
f"The DotProduct2CodeTrans transformation only supports "
f"the transformation of a dotproduct intrinsic if its "
f"arguments are plain arrays, but found "
f"{arg.debug_string()} in {node.debug_string()}.")
for arg in node.arguments:
# The argument should be a 1D array if the argument does
# not provide any array slice information (i.e. it is a
# Reference)
if arg.__class__ is Reference:
symbol = arg.symbol
# This symbol should be a 1D array
if not (isinstance(symbol, DataSymbol) and symbol.is_array and
len(symbol.shape) == 1):
raise TransformationError(
f"The DotProduct2CodeTrans transformation only "
f"supports the transformation of a dotproduct "
f"intrinsic with an argument not containing an array "
f"slice if the argument is a 1D array, but found "
f"{arg.debug_string()} with {len(symbol.shape)} "
f"dimensions in {node.debug_string()}.")
for arg in node.children:
# If the argument does provide array slice information
# then check the array slice is in the first dimension of
# the array and that the slice is for the full range of that
# dimension i.e. uses a ':'.
if isinstance(arg, ArrayReference):
if not isinstance(arg.indices[0], Range):
raise TransformationError(
f"The DotProduct2CodeTrans transformation only "
f"supports the transformation of a dotproduct "
f"intrinsic with an argument containing an array "
f"slice if the array slice is for the 1st dimension "
f"of the array, but found {arg.debug_string()} in "
f"{node.debug_string()}.")
if not arg.is_full_range(0):
raise TransformationError(
f"The DotProduct2CodeTrans transformation only "
f"supports the transformation of a dotproduct "
f"intrinsic with an argument containing an array "
f"slice if the argument is for the 1st dimension "
f"of the array and is for the full range of that "
f"dimension, but found {arg.debug_string()} in "
f"{node.debug_string()}.")
# Both arguments should be real (as other intrinsic datatypes
# are not supported).
for arg in node.arguments:
if arg.symbol.datatype.intrinsic != ScalarType.Intrinsic.REAL:
raise TransformationError(
f"The DotProduct2CodeTrans transformation only supports "
f"arrays of real data, but found {arg.debug_string()} of "
f"type {arg.symbol.datatype.intrinsic.name} in "
f"{node.debug_string()}.")
[docs]
def apply(self, node, options=None, **kwargs):
'''Apply the DOT_PRODUCT intrinsic conversion transformation to the
specified node. This node must be a DOT_PRODUCT
BinaryOperation. If the transformation is successful then an
assignment which includes a DOT_PRODUCT BinaryOperation node
is converted to equivalent inline code.
:param node: a DOT_PRODUCT Binary-Operation node.
:type node: :py:class:`psyclone.psyir.nodes.BinaryOperation`
:param options: a dictionary with options for transformations.
:type options: dict of str:str or None
'''
self.validate(node, options, **kwargs)
assignment = node.ancestor(Assignment)
vector1 = node.arguments[0]
vector2 = node.arguments[1]
symbol_table = node.ancestor(Routine).symbol_table
# Create new i loop iterator.
i_loop_symbol = symbol_table.new_symbol(
"i", symbol_type=DataSymbol, datatype=INTEGER_TYPE)
# Create temporary result variable. Use the datatype of one of
# the arguments. We can do this as the validate method only
# allows plain real arrays.
vector1_datatype = vector1.symbol.datatype
datatype = ScalarType(
vector1_datatype.intrinsic, vector1_datatype.precision)
symbol_res_var = symbol_table.new_symbol(
"res_dot_product", symbol_type=DataSymbol,
datatype=datatype)
# Replace operation with the temporary result variable.
result_ref = Reference(symbol_res_var)
node.replace_with(result_ref)
# Create "vector1(i)"
vector1_dims = [Reference(i_loop_symbol)]
if len(vector1.children) > 1:
# Add any additional dimensions (in case of an array slice)
for child in vector1.children[1:]:
vector1_dims.append(child.copy())
vector1_array_reference = ArrayReference.create(
vector1.symbol, vector1_dims)
# Create "vector2(i)"
vector2_dims = [Reference(i_loop_symbol)]
if len(vector2.children) > 1:
# Add any additional dimensions (in case of an array slice)
for child in vector2.children[1:]:
vector2_dims.append(child.copy())
vector2_array_reference = ArrayReference.create(
vector2.symbol, vector2_dims)
# Create "vector1(i) * vector2(i)"
multiply = BinaryOperation.create(
BinaryOperation.Operator.MUL, vector1_array_reference,
vector2_array_reference)
# Create "result + vector1(i) * vector2(i)"
rhs = BinaryOperation.create(
BinaryOperation.Operator.ADD, result_ref.copy(), multiply)
# Create "result = result + vector1(i) * vector2(i)"
assign = Assignment.create(result_ref.copy(), rhs)
# Work out the loop bounds. We use the declaration that has the most
# information (i.e. explicit bounds).
bounds1 = vector1.symbol.get_bounds(0)
bounds2 = vector2.symbol.get_bounds(0)
score1 = sum(1 for bnd in bounds1 if isinstance(bnd, IntrinsicCall))
score2 = sum(1 for bnd in bounds2 if isinstance(bnd, IntrinsicCall))
if score1 <= score2:
lower_bound, upper_bound = bounds1[0], bounds1[1]
else:
lower_bound, upper_bound = bounds2[0], bounds2[1]
# Create i loop and add the above code as a child
iloop = Loop.create(i_loop_symbol, lower_bound,
upper_bound, Literal("1", INTEGER_TYPE),
[assign])
# Create "result = 0.0"
assign = Assignment.create(result_ref.copy(),
Literal("0.0", REAL_TYPE))
# Add the initialisation and loop nodes into the PSyIR tree
assignment.parent.children.insert(assignment.position, assign)
assignment.parent.children.insert(assignment.position, iloop)
# For AutoAPI auto-documentation generation.
__all__ = ["DotProduct2CodeTrans"]