diff --git a/.github/workflows/nemo_tests.yml b/.github/workflows/nemo_tests.yml index 79aab92ba2..be2d17d5f1 100644 --- a/.github/workflows/nemo_tests.yml +++ b/.github/workflows/nemo_tests.yml @@ -94,6 +94,13 @@ jobs: module load perl/${PERL_VERSION} make -j 4 passthrough make -j 4 compile-passthrough + make run-passthrough |& tee output.txt + # Check the output is as expected for the first 6 digits (if not exit with error message) + tail -n 1 output.txt | grep -q " it : 10" || (echo "Error: 'it : 10' not found!" & false) + tail -n 1 output.txt | grep -q "|ssh|_max: 0.259483" || (echo "Error: '|ssh|_max: 0.259483' not found!" & false) + tail -n 1 output.txt | grep -q "|U|_max: 0.458515" || (echo "Error: '|U|_max: 0.458515' not found!" & false) + tail -n 1 output.txt | grep -q "S_min: 0.482686" || (echo "Error: 'S_min: 0.482686' not found!" & false) + tail -n 1 output.txt | grep -q "S_max: 0.407622" || (echo "Error: 'S_max: 0.407622' not found!" & false) # PSyclone, compile and run MetOffice NEMO with OpenMP for GPUs - name: NEMO MetOffice OpenMP for GPU diff --git a/doc/user_guide/transformations.rst b/doc/user_guide/transformations.rst index 635773d51f..609d97d7fa 100644 --- a/doc/user_guide/transformations.rst +++ b/doc/user_guide/transformations.rst @@ -568,6 +568,13 @@ can be found in the API-specific sections). #### +.. autoclass:: psyclone.psyir.transformations.ScalarizationTrans + :members: apply + :noindex: + +#### + + Algorithm-layer --------------- diff --git a/examples/nemo/scripts/passthrough.py b/examples/nemo/scripts/passthrough.py index 26eb480722..f998904431 100755 --- a/examples/nemo/scripts/passthrough.py +++ b/examples/nemo/scripts/passthrough.py @@ -37,10 +37,21 @@ ''' Process Nemo code with PSyclone but don't do any changes. This file is only needed to provide a FILES_TO_SKIP list. ''' from utils import PASSTHROUGH_ISSUES +# TODO Remove +from psyclone.psyir.transformations import ScalarizationTrans +from psyclone.psyir.nodes import Routine, Loop # List of all files that psyclone will skip processing FILES_TO_SKIP = PASSTHROUGH_ISSUES -def trans(_): +def trans(psyir): ''' Don't do any changes. ''' + if psyir.name.startswith("obs_"): + print("Skipping file", psyir.name) + return + # TODO REMOVE + for subroutine in psyir.walk(Routine): + scalartrans = ScalarizationTrans() + for loop in subroutine.walk(Loop): + scalartrans.apply(loop) diff --git a/src/psyclone/psyir/nodes/node.py b/src/psyclone/psyir/nodes/node.py index 3567805131..359e776324 100644 --- a/src/psyclone/psyir/nodes/node.py +++ b/src/psyclone/psyir/nodes/node.py @@ -1782,6 +1782,23 @@ def update_parent_symbol_table(self, new_parent): ''' + def is_descendent_of(self, potential_ancestor) -> bool: + ''' + Checks if this node is a descendant of the `potential_ancestor` node. + + :param potential_ancestor: The Node to check whether its an ancestor + of self. + :type node: :py:class:`psyclone.psyir.nodes.Node` + + :returns: whether potential_ancestor is an ancestor of this node. + ''' + current_node = self + while (current_node is not potential_ancestor and + current_node.parent is not None): + current_node = current_node.parent + + return current_node is potential_ancestor + # For automatic documentation generation # TODO #913 the 'colored' routine shouldn't be in this module. diff --git a/src/psyclone/psyir/tools/definition_use_chains.py b/src/psyclone/psyir/tools/definition_use_chains.py index 6a68f29233..c480b625cf 100644 --- a/src/psyclone/psyir/tools/definition_use_chains.py +++ b/src/psyclone/psyir/tools/definition_use_chains.py @@ -250,16 +250,20 @@ def find_forward_accesses(self): .abs_position + 1 ) - # We make a copy of the reference to have a detached - # node to avoid handling the special cases based on - # the parents of the reference. - chain = DefinitionUseChain( - self._reference.copy(), - body, - start_point=ancestor.abs_position, - stop_point=sub_stop_point, - ) - chains.insert(0, chain) + # If we have a basic block with no children then skip it, + # e.g. for an if block with no code before the else + # statement, as is found in NEMO4. + if len(body) > 0: + # We make a copy of the reference to have a detached + # node to avoid handling the special cases based on + # the parents of the reference. + chain = DefinitionUseChain( + self._reference.copy(), + body, + start_point=ancestor.abs_position, + stop_point=sub_stop_point, + ) + chains.insert(0, chain) # If its a while loop, create a basic block for the while # condition. if isinstance(ancestor, WhileLoop): @@ -300,6 +304,11 @@ def find_forward_accesses(self): # Now add all the other standardly handled basic_blocks to the # list of chains. for block in basic_blocks: + # If we have a basic block with no children then skip it, + # e.g. for an if block with no code before the else + # statement, as is found in NEMO4. + if len(block) == 0: + continue chain = DefinitionUseChain( self._reference, block, @@ -835,6 +844,11 @@ def find_backward_accesses(self): # Now add all the other standardly handled basic_blocks to the # list of chains. for block in basic_blocks: + # If we have a basic block with no children then skip it, + # e.g. for an if block with no code before the else + # statement, as is found in NEMO4. + if len(block) == 0: + continue chain = DefinitionUseChain( self._reference, block, @@ -874,14 +888,18 @@ def find_backward_accesses(self): ).abs_position else: sub_start_point = self._reference.abs_position - chain = DefinitionUseChain( - self._reference.copy(), - body, - start_point=sub_start_point, - stop_point=sub_stop_point, - ) - chains.append(chain) - control_flow_nodes.append(ancestor) + # If we have a basic block with no children then skip it, + # e.g. for an if block with no code before the else + # statement, as is found in NEMO4. + if len(body) > 0: + chain = DefinitionUseChain( + self._reference.copy(), + body, + start_point=sub_start_point, + stop_point=sub_stop_point, + ) + chains.append(chain) + control_flow_nodes.append(ancestor) # If its a while loop, create a basic block for the while # condition. if isinstance(ancestor, WhileLoop): diff --git a/src/psyclone/psyir/transformations/__init__.py b/src/psyclone/psyir/transformations/__init__.py index 70691de0d1..40f31bd2a2 100644 --- a/src/psyclone/psyir/transformations/__init__.py +++ b/src/psyclone/psyir/transformations/__init__.py @@ -105,6 +105,8 @@ ReplaceInductionVariablesTrans from psyclone.psyir.transformations.reference2arrayrange_trans import \ Reference2ArrayRangeTrans +from psyclone.psyir.transformations.scalarization_trans import \ + ScalarizationTrans # For AutoAPI documentation generation @@ -145,5 +147,6 @@ 'Reference2ArrayRangeTrans', 'RegionTrans', 'ReplaceInductionVariablesTrans', + 'ScalarizationTrans', 'TransformationError', 'ValueRangeCheckTrans'] diff --git a/src/psyclone/psyir/transformations/scalarization_trans.py b/src/psyclone/psyir/transformations/scalarization_trans.py new file mode 100644 index 0000000000..a47c8b9f38 --- /dev/null +++ b/src/psyclone/psyir/transformations/scalarization_trans.py @@ -0,0 +1,303 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2024-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: A. B. G. Chalk, STFC Daresbury Lab + +'''This module provides the sclarization transformation class.''' + +import itertools +from typing import Optional, Dict, Any + +from psyclone.core import VariablesAccessInfo, Signature +from psyclone.psyGen import Kern +from psyclone.psyir.nodes import Call, CodeBlock, \ + Loop, Reference, Routine, StructureReference +from psyclone.psyir.symbols import DataSymbol, RoutineSymbol +from psyclone.psyir.transformations.loop_trans import LoopTrans + + +class ScalarizationTrans(LoopTrans): + '''This transformation takes a Loop and converts any array accesses + to scalar if the results of the loop are unused, and the initial value + is unused. For example: + + >>> from psyclone.psyir.backend.fortran import FortranWriter + >>> from psyclone.psyir.frontend.fortran import FortranReader + >>> from psyclone.psyir.transformations import ScalarizationTrans + >>> from psyclone.psyir.nodes import Loop + >>> code = """program test + ... integer :: i,j + ... real :: a(100), b(100) + ... do i = 1,100 + ... a(i) = i + ... b(i) = a(i) * a(i) + ... end do + ... do j = 1, 100 + ... if(b(i) > 200) then + ... print *, b(i) + ... end if + ... end do + ... end program""" + >>> psyir = FortranReader().psyir_from_source(code) + >>> scalarise = ScalarizationTrans() + >>> scalarise.apply(psyir.walk(Loop)[0]) + >>> print(FortranWriter()(psyir)) + program test + integer :: i + integer :: j + real, dimension(100) :: a + real, dimension(100) :: b + real :: a_scalar + + do i = 1, 100, 1 + a_scalar = i + b(i) = a_scalar * a_scalar + enddo + do j = 1, 100, 1 + if (b(i) > 200) then + ! PSyclone CodeBlock (unsupported code) reason: + ! - Unsupported statement: Print_Stmt + PRINT *, b(i) + end if + enddo + + end program test + + ''' + + @staticmethod + def _is_local_array(signature: Signature, + var_accesses: VariablesAccessInfo) -> bool: + ''' + :param signature: The signature to check if it is a local array symbol + or not. + :param var_accesses: The VariableAccessesInfo object containing + signature. + :returns: whether the symbol corresponding to signature is a + local array symbol or not. + ''' + if not var_accesses[signature].is_array(): + return False + # If any of the accesses are to a CodeBlock then we stop. This can + # happen if there is a string access inside a string concatenation, + # e.g. NEMO4. + for access in var_accesses[signature].all_accesses: + if isinstance(access.node, CodeBlock): + return False + base_symbol = var_accesses[signature].all_accesses[0].node.symbol + if not base_symbol.is_automatic: + return False + # If its a derived type then we don't scalarize. + if isinstance(var_accesses[signature].all_accesses[0].node, + StructureReference): + return False + # Find the containing routine + rout = var_accesses[signature].all_accesses[0].node.ancestor(Routine) + # If the array is the return symbol then its not a local + # array symbol + if base_symbol is rout.return_symbol: + return False + + return True + + @staticmethod + def _have_same_unmodified_index( + signature: Signature, + var_accesses: VariablesAccessInfo) -> bool: + ''' + :param signature: The signature to check. + :param var_accesses: The VariableAccessesInfo object containing + signature. + :returns: whether all the array accesses to signature use the + same index, and whether the index is unmodified in + the code region. + ''' + array_indices = None + scalarizable = True + for access in var_accesses[signature].all_accesses: + if array_indices is None: + array_indices = access.component_indices + # For some reason using == on the component_lists doesn't work + # so we use [:] notation. + elif array_indices[:] != access.component_indices[:]: + scalarizable = False + break + # For each index, we need to check they're not written to in + # the loop. + flattened_indices = list(itertools.chain.from_iterable( + array_indices)) + for index in flattened_indices: + # Index may not be a Reference, so we need to loop over the + # References + for ref in index.walk(Reference): + # This Reference could be the symbol for a Call or + # IntrinsicCall, which we don't allow to scalarize + if isinstance(ref.symbol, RoutineSymbol): + scalarizable = False + break + sig, _ = ref.get_signature_and_indices() + if var_accesses[sig].is_written(): + scalarizable = False + break + + return scalarizable + + @staticmethod + def _check_first_access_is_write(signature: Signature, + var_accesses: VariablesAccessInfo) \ + -> bool: + ''' + :param signature: The signature to check. + :param var_accesses: The VariableAccessesInfo object containing + signature. + :returns: whether the first access to signature is a write. + ''' + if var_accesses[signature].is_written_first(): + return True + return False + + @staticmethod + def _value_unused_after_loop(sig: Signature, + loop: Loop, + var_accesses: VariablesAccessInfo) -> bool: + ''' + :param sig: The signature to check. + :param loop: The loop the transformation is operating on. + :param var_accesses: The VariableAccessesInfo object containing + signature. + :returns: whether the value computed in the loop containing + sig is read from after the loop. + ''' + # Find the last access of the signature + last_access = var_accesses[sig].all_accesses[-1].node + # Find the next accesses to this symbol + next_accesses = last_access.next_accesses() + for next_access in next_accesses: + # next_accesses looks backwards to the start of the loop, + # but we don't care about those accesses here. + if next_access.is_descendent_of(loop): + continue + + # If next access is a Call or CodeBlock or Kern then + # we have to assume the value is used. These nodes don't + # have the is_read property that Reference has, so we need + # to be explicit. + if isinstance(next_access, (CodeBlock, Call, Kern)): + return False + + # If the access is a read, then return False + if next_access.is_read: + return False + + return True + + def apply(self, node: Loop, options: Optional[Dict[str, Any]] = None) \ + -> None: + ''' + Apply the scalarization transformation to a loop. + All of the array accesses that are identified as being able to be + scalarized will be transformed by this transformation. + + An array access will be scalarized if: + 1. All accesses to the array use the same indexing statement. + 2. All References contained in the indexing statement are not modified + inside of the loop (loop variables are ok). + 3. The array symbol is either not accessed again or is written to + as its next access. If the next access is inside a conditional + that is not an ancestor of the input loop, then PSyclone will + assume that we cannot scalarize that value instead of attempting to + understand the control flow. + 4. The array symbol is a local variable. + + :param node: the supplied loop to apply scalarization to. + :param options: a dictionary with options for transformations. + + ''' + # For each array reference in the Loop: + # Find every access to the same symbol in the loop + # They all have to be accessed with the same index statement, and + # that index needs to not be written to inside the loop body. + # For each symbol that meets this criteria, we then need to check the + # first access is a write + # Then, for each symbol still meeting this criteria, we need to find + # the next access outside of this loop. If its inside an ifblock that + # is not an ancestor of this loop then we refuse to scalarize for + # simplicity. Otherwise if its a read we can't scalarize safely. + # If its a write then this symbol can be scalarized. + + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + + # Find all the ararys that are only accessed by a single index, and + # that index is only read inside the loop. + potential_targets = filter( + lambda sig: + ScalarizationTrans._is_local_array(sig, var_accesses), + var_accesses) + potential_targets = filter( + lambda sig: + ScalarizationTrans._have_same_unmodified_index(sig, + var_accesses), + potential_targets) + + # Now we need to check the first access is a write and remove those + # that aren't. + potential_targets = filter( + lambda sig: + ScalarizationTrans._check_first_access_is_write(sig, + var_accesses), + potential_targets) + + # Check the values written to these arrays are not used after this loop + finalised_targets = filter( + lambda sig: + ScalarizationTrans._value_unused_after_loop(sig, + node, + var_accesses), + potential_targets) + + routine_table = node.ancestor(Routine).symbol_table + # For each finalised target we can replace them with a scalarized + # symbol + for target in finalised_targets: + target_accesses = var_accesses[target].all_accesses + first_access = target_accesses[0].node + symbol_type = first_access.symbol.datatype.datatype + symbol_name = first_access.symbol.name + scalar_symbol = routine_table.new_symbol( + root_name=f"{symbol_name}_scalar", + symbol_type=DataSymbol, + datatype=symbol_type) + ref_to_copy = Reference(scalar_symbol) + for access in target_accesses: + node = access.node + node.replace_with(ref_to_copy.copy()) diff --git a/src/psyclone/tests/psyir/nodes/node_test.py b/src/psyclone/tests/psyir/nodes/node_test.py index 8ba43b668f..7d3714df73 100644 --- a/src/psyclone/tests/psyir/nodes/node_test.py +++ b/src/psyclone/tests/psyir/nodes/node_test.py @@ -1904,3 +1904,22 @@ def test_following(fortran_reader): assert routines[1] not in loops[1].following(include_children=False) assert routines[1] in loops[1].following(same_routine_scope=False, include_children=False) + + +def test_is_descendent_of(fortran_reader): + '''Test the is_descendent_of function of the Node class''' + code = ''' + subroutine test() + integer :: i, j, k + do i = 1, 100 + j = i + end do + k = 1 + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + loop = psyir.walk(Loop)[0] + assigns = psyir.walk(Assignment) + + assert assigns[0].is_descendent_of(loop) + assert not assigns[1].is_descendent_of(loop) diff --git a/src/psyclone/tests/psyir/tools/definition_use_chains_backward_dependence_test.py b/src/psyclone/tests/psyir/tools/definition_use_chains_backward_dependence_test.py index bafc2c92c8..85fa66499a 100644 --- a/src/psyclone/tests/psyir/tools/definition_use_chains_backward_dependence_test.py +++ b/src/psyclone/tests/psyir/tools/definition_use_chains_backward_dependence_test.py @@ -687,3 +687,34 @@ def test_definition_use_chains_backward_accesses_nonassign_reference_in_loop( assert reaches[0] is routine.children[1].loop_body.children[1].children[1] assert reaches[1] is routine.children[1].loop_body.children[0].lhs assert reaches[2] is routine.children[0].lhs + + +def test_definition_use_chains_backward_accesses_empty_schedules( + fortran_reader, +): + '''Coverage to handle the case where we have empty schedules inside + various type of code.''' + code = """ + subroutine x() + integer :: a, i + a = 1 + do i = 1, 100 + end do + if(.TRUE.) then + else + endif + do while(.FALSE.) + end do + a = a + a + end subroutine x + """ + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + chains = DefinitionUseChain( + routine.children[4].lhs + ) + reaches = chains.find_backward_accesses() + assert len(reaches) == 3 + assert reaches[0] is routine.children[4].rhs.children[1] + assert reaches[1] is routine.children[4].rhs.children[0] + assert reaches[2] is routine.children[0].lhs diff --git a/src/psyclone/tests/psyir/tools/definition_use_chains_forward_dependence_test.py b/src/psyclone/tests/psyir/tools/definition_use_chains_forward_dependence_test.py index 6cf86a3874..bb122de94a 100644 --- a/src/psyclone/tests/psyir/tools/definition_use_chains_forward_dependence_test.py +++ b/src/psyclone/tests/psyir/tools/definition_use_chains_forward_dependence_test.py @@ -937,3 +937,34 @@ def test_definition_use_chains_forward_accesses_multiple_routines( ) reaches = chains.find_forward_accesses() assert len(reaches) == 0 + + +def test_definition_use_chains_forward_accesses_empty_schedules( + fortran_reader, +): + '''Coverage to handle the case where we have empty schedules inside + various type of code.''' + code = """ + subroutine x() + integer :: a, i + a = 1 + do i = 1, 100 + end do + if(.TRUE.) then + else + endif + do while(.FALSE.) + end do + a = a + a + end subroutine x + """ + psyir = fortran_reader.psyir_from_source(code) + routine = psyir.walk(Routine)[0] + chains = DefinitionUseChain( + routine.children[0].lhs + ) + reaches = chains.find_forward_accesses() + assert len(reaches) == 3 + assert reaches[0] is routine.children[4].rhs.children[0] + assert reaches[1] is routine.children[4].rhs.children[1] + assert reaches[2] is routine.children[4].lhs diff --git a/src/psyclone/tests/psyir/transformations/scalarization_trans_test.py b/src/psyclone/tests/psyir/transformations/scalarization_trans_test.py new file mode 100644 index 0000000000..367a9aa93f --- /dev/null +++ b/src/psyclone/tests/psyir/transformations/scalarization_trans_test.py @@ -0,0 +1,618 @@ +# ----------------------------------------------------------------------------- +# BSD 3-Clause License +# +# Copyright (c) 2024-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: A. B. G. Chalk, STFC Daresbury Lab + +'''This module tests the scalarization transformation. +''' + +from psyclone.core import VariablesAccessInfo +from psyclone.psyir.transformations import ScalarizationTrans +from psyclone.tests.utilities import Compile + + +def test_scalararizationtrans_is_local_array(fortran_reader): + code = '''function test(a) result(x) + use mymod, only: arr, atype + integer :: i + integer :: k + real, dimension(1:100) :: local + real, dimension(1:100) :: a + character(2), dimension(1:100) :: b + real, dimension(1:100) :: x + type(atype) :: custom + type(atype), dimension(1:100) :: custom2 + + do i = 1, 100 + arr(i) = i + a(i) = i + local(i) = i + b(i) = b(i) // "c" + x(i) = i + custom%type(i) = i + custom2(i)%typeb(i) = i + end do + end function''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert not ScalarizationTrans._is_local_array(keys[1], + var_accesses) + # Test a + assert var_accesses[keys[2]].var_name == "a" + assert not ScalarizationTrans._is_local_array(keys[2], + var_accesses) + # Test local + assert var_accesses[keys[3]].var_name == "local" + assert ScalarizationTrans._is_local_array(keys[3], + var_accesses) + + # Test b - the RHS of the assignment is a codeblock so we do not + # count it as a local array and invalidate it, as otherwise the + # local array test can fail. Also we can't safely transform the + # CodeBlock anyway. + assert var_accesses[keys[4]].var_name == "b" + assert not ScalarizationTrans._is_local_array(keys[4], + var_accesses) + + # Test x - the return value is not classed as a local array. + assert var_accesses[keys[5]].var_name == "x" + assert not ScalarizationTrans._is_local_array(keys[5], + var_accesses) + + # Test custom - we don't scalarize derived types. + assert var_accesses[keys[6]].var_name == "custom%type" + assert not ScalarizationTrans._is_local_array(keys[6], + var_accesses) + # Test custom2 - we don't scalarize derived types. + assert var_accesses[keys[7]].var_name == "custom2%typeb" + assert not ScalarizationTrans._is_local_array(keys[7], + var_accesses) + + # Test filter behaviour same as used in the transformation + local_arrays = filter( + lambda sig: ScalarizationTrans._is_local_array(sig, var_accesses), + var_accesses) + local_arrays = list(local_arrays) + assert len(local_arrays) == 1 + assert local_arrays[0].var_name == "local" + + +def test_scalarizationtrans_have_same_unmodified_index(fortran_reader): + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: a + real, dimension(1:103) :: b + real, dimension(1:100) :: c + k = 0 + do i = 1, 100 + a(i) = i + b(i+2) = i + b(i+3) = b(i) + b(i+1) + c(k) = 2 + k = k + 1 + end do + end subroutine''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[1] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test a + assert var_accesses[keys[1]].var_name == "a" + assert ScalarizationTrans._have_same_unmodified_index(keys[1], + var_accesses) + # Test b (differeing indices) + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._have_same_unmodified_index(keys[2], + var_accesses) + # Test c (k is modified) + assert var_accesses[keys[3]].var_name == "c" + assert not ScalarizationTrans._have_same_unmodified_index(keys[3], + var_accesses) + # Test filter behaviour same as used in the transformation + local_arrays = filter( + lambda sig: ScalarizationTrans._is_local_array(sig, var_accesses), + var_accesses) + local_arrays = list(local_arrays) + assert len(local_arrays) == 3 + + unmodified_indices = filter( + lambda sig: ScalarizationTrans._have_same_unmodified_index( + sig, var_accesses), + local_arrays) + unmodified_indices = list(unmodified_indices) + assert len(unmodified_indices) == 1 + assert unmodified_indices[0].var_name == "a" + + +def test_scalarizationtrans_check_first_access_is_write(fortran_reader): + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: a + real, dimension(1:100) :: b + real, dimension(1:100) :: c + do i = 1, 100 + a(i) = i + b(i) = b(i) + 1 + c(i) = a(i) + b(i) + end do + end subroutine''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test a + assert var_accesses[keys[1]].var_name == "a" + assert ScalarizationTrans._check_first_access_is_write(keys[1], + var_accesses) + # Test b (differeing indices) + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._check_first_access_is_write(keys[2], + var_accesses) + # Test c (k is modified) + assert var_accesses[keys[3]].var_name == "c" + assert ScalarizationTrans._check_first_access_is_write(keys[3], + var_accesses) + + # Test filter behaviour same as used in the transformation + local_arrays = filter( + lambda sig: ScalarizationTrans._is_local_array(sig, var_accesses), + var_accesses) + local_arrays = list(local_arrays) + assert len(local_arrays) == 3 + + unmodified_indices = filter( + lambda sig: ScalarizationTrans._have_same_unmodified_index( + sig, var_accesses), + local_arrays) + unmodified_indices = list(unmodified_indices) + assert len(unmodified_indices) == 3 + + first_write_arrays = filter( + lambda sig: ScalarizationTrans._check_first_access_is_write( + sig, var_accesses), + unmodified_indices) + first_write_arrays = list(first_write_arrays) + assert len(first_write_arrays) == 2 + + +def test_scalarizationtrans_value_unused_after_loop(fortran_reader): + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + end do + do i = 1, 100 + b(i) = b(i) + 1 + end do + end subroutine + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + + # Test we ignore array next_access if they're in an if statement + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + logical :: x = .FALSE. + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + end do + if(x) then + do i = 1, 100 + b(i) = 1 + end do + end if + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + # Test we don't ignore array next_access if they're in an if statement + # that is an ancestor of the loop we're scalarizing + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + + if(.false.) then + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + end do + do i = 1, 100 + b(i) = 1 + end do + end if + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0].if_body.children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + + # Test we don't ignore array next_access if they have an ancestor + # that is a Call + code = '''subroutine test() + use my_mod + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + + if(.false.) then + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + end do + do i = 1, 100 + Call some_func(b(i)) + end do + end if + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0].if_body.children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + + # Test being a while condition correctly counts as being used. + code = '''subroutine test() + use my_mod + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + end do + do i = 1, 100 + do while(b(i) < 256) + b(i) = arr(i) * arr(i) + arr(i) = arr(i) * 2 + end do + end do + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + + # Test being a loop start/stop/step condition correctly counts + # as being used. + code = '''subroutine test() + use my_mod + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + real, dimension(1:100) :: c + real, dimension(1:100, 1:100) :: d + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + c(i) = i + end do + do i = 1, 100 + do k = arr(i), b(i), c(i) + d(i,k) = i + end do + end do + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert not ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + # Test b + assert var_accesses[keys[2]].var_name == "b" + assert not ScalarizationTrans._value_unused_after_loop(keys[2], + node.loop_body, + var_accesses) + # Test c + assert var_accesses[keys[3]].var_name == "c" + assert not ScalarizationTrans._value_unused_after_loop(keys[3], + node.loop_body, + var_accesses) + + # Test being a symbol in a Codeblock counts as used + code = '''subroutine test() + use my_mod + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + real, dimension(1:100) :: c + real, dimension(1:100, 1:100) :: d + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + c(i) = i + end do + do i = 1, 100 + print *, arr(i) + end do + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert not ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + + # Test being in an IfBlock condition counts as used. + code = '''subroutine test() + use my_mod + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + real, dimension(1:100) :: c + real, dimension(1:100, 1:100) :: d + + do i = 1, 100 + arr(i) = exp(arr(i)) + b(i) = arr(i) * 3 + c(i) = i + end do + do i = 1, 100 + if(arr(i) == 1) then + print *, b(i) + end if + end do + end subroutine test + ''' + psyir = fortran_reader.psyir_from_source(code) + node = psyir.children[0].children[0] + var_accesses = VariablesAccessInfo(nodes=node.loop_body) + keys = list(var_accesses.keys()) + # Test arr + assert var_accesses[keys[1]].var_name == "arr" + assert not ScalarizationTrans._value_unused_after_loop(keys[1], + node.loop_body, + var_accesses) + + +def test_scalarization_trans_apply(fortran_reader, fortran_writer, tmpdir): + ''' Test the application of the scalarization transformation.''' + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + real, dimension(1:100) :: c + + do i = 1, 100 + arr(i) = i + arr(i) = exp(arr(i)) + k = i + b(i) = arr(i) * 3 + c(k) = i + end do + do i = 1, 100 + b(i) = b(i) + 1 + end do + end subroutine + ''' + strans = ScalarizationTrans() + psyir = fortran_reader.psyir_from_source(code) + + loop = psyir.children[0].children[0] + strans.apply(loop) + correct = '''subroutine test() + integer :: i + integer :: k + real, dimension(100) :: arr + real, dimension(100) :: b + real, dimension(100) :: c + real :: arr_scalar + + do i = 1, 100, 1 + arr_scalar = i + arr_scalar = EXP(arr_scalar) + k = i + b(i) = arr_scalar * 3 + c(k) = i + enddo + do i = 1, 100, 1 + b(i) = b(i) + 1 + enddo''' + out = fortran_writer(psyir) + assert correct in out + assert Compile(tmpdir).string_compiles(out) + + # Use in if/else where the if has write only followup and + # the else has a read - shouldn't scalarise b. + code = '''subroutine test() + integer :: i + integer :: k + real, dimension(1:100) :: arr + real, dimension(1:100) :: b + real, dimension(1:100) :: c + + do i = 1, 100 + arr(i) = i + arr(i) = exp(arr(i)) + k = i + b(i) = arr(i) * 3 + c(k) = i + end do + do i = 1, 100 + if(c(i) > 50) then + b(i) = c(i) + else + b(i) = b(i) + c(i) + end if + end do + end subroutine + ''' + strans = ScalarizationTrans() + psyir = fortran_reader.psyir_from_source(code) + + loop = psyir.children[0].children[0] + strans.apply(loop) + correct = '''subroutine test() + integer :: i + integer :: k + real, dimension(100) :: arr + real, dimension(100) :: b + real, dimension(100) :: c + real :: arr_scalar + + do i = 1, 100, 1 + arr_scalar = i + arr_scalar = EXP(arr_scalar) + k = i + b(i) = arr_scalar * 3 + c(k) = i + enddo + do i = 1, 100, 1 + if (c(i) > 50) then + b(i) = c(i) + else + b(i) = b(i) + c(i) + end if + enddo''' + out = fortran_writer(psyir) + assert correct in out + assert Compile(tmpdir).string_compiles(out) + + +def test_scalarization_trans_apply_routinesymbol(fortran_reader, + fortran_writer, tmpdir): + ''' Test the application of the scalarization transformation doesn't work + when applied on an array with a RoutineSymbol as an index.''' + code = '''subroutine test + integer, dimension(3) :: j + integer :: i + integer, allocatable, dimension(:,:,:) :: k + do i= 1, 100 + allocate(k(MAXVAL(j(1:3)),1,1)) + deallocate(k) + end do + end subroutine test''' + strans = ScalarizationTrans() + psyir = fortran_reader.psyir_from_source(code) + strans.apply(psyir.children[0].children[0]) + correct = '''subroutine test() + integer, dimension(3) :: j + integer :: i + integer, allocatable, dimension(:,:,:) :: k + + do i = 1, 100, 1 + ALLOCATE(k(1:MAXVAL(j(:)),1:1,1:1)) + DEALLOCATE(k) + enddo + +end subroutine test +''' + out = fortran_writer(psyir) + assert correct == out + assert Compile(tmpdir).string_compiles(out)