Repository: stfc/PSyclone
Pull request #2465 (2024-01-11 13:48:55)

(towards 2004) Move same_range functionality to ArrayMixin and other improvements
This is some work in preparation of merging the two implementations we have of ArrayRange2Loop in #2004, but it is big enough to be a standalone PR:
- It moves the same_range implementation outside the arrayrange transformation, because it seems this functionality is more generic.
- It allows psyir_from_expression/statement methods to work without a symbol_table argument, because it is convenient for tests where we do not care about the symbol declarations.
- It modernises method signatures using type hints
- Adds some xfail test to demonstrate cases when this functionality is still lacking



Patch:
diff --git a/changelog b/changelog
index c8add54a3..d59f6ba73 100644
--- a/changelog
+++ b/changelog
@@ -149,6 +149,9 @@
 
 	54) PR #2493 for #2492. Fix bug in NemoOuterArrayRange2LoopTrans
 
+	55) PR #2465 towards #2004. Move same_range functionality to
+	ArrayMixin and other improvements.
+
 release 2.4.0 29th of September 2023
 
 	1) PR #1758 for #1741. Splits the PSyData read functionality into a
diff --git a/src/psyclone/psyir/frontend/fortran.py b/src/psyclone/psyir/frontend/fortran.py
index 357fec916..9eb7963c1 100644
--- a/src/psyclone/psyir/frontend/fortran.py
+++ b/src/psyclone/psyir/frontend/fortran.py
@@ -36,6 +36,7 @@
 
 ''' This module provides the PSyIR Fortran front-end.'''
 
+from typing import Optional
 from fparser.common.readfortran import FortranStringReader, FortranFileReader
 from fparser.common.sourceinfo import FortranFormat
 from fparser.two import Fortran2003, pattern_tools
@@ -63,12 +64,12 @@ class FortranReader():
         SYMBOL_TABLES.clear()
 
     @staticmethod
-    def validate_name(name):
+    def validate_name(name: str):
         '''
         Utility method that checks that the supplied name is a valid
         Fortran name.
 
-        :param str name: the name to check.
+        :param name: the name to check.
 
         :raises TypeError: if the name is not a string.
         :raises ValueError: if this is not a valid name.
@@ -82,11 +83,11 @@ class FortranReader():
             raise ValueError(
                 f"Invalid Fortran name '{name}' found.")
 
-    def psyir_from_source(self, source_code, free_form=True):
+    def psyir_from_source(self, source_code: str, free_form: bool = True):
         ''' Generate the PSyIR tree representing the given Fortran source code.
 
-        :param str source_code: text representation of the code to be parsed.
-        :param bool free_form: If parsing free-form code or not (default True).
+        :param source_code: text representation of the code to be parsed.
+        :param free_form: If parsing free-form code or not (default True).
 
         :returns: PSyIR representing the provided Fortran source code.
         :rtype: :py:class:`psyclone.psyir.nodes.Node`
@@ -100,15 +101,15 @@ class FortranReader():
         psyir = self._processor.generate_psyir(parse_tree)
         return psyir
 
-    def psyir_from_expression(self, source_code, symbol_table):
+    def psyir_from_expression(self, source_code: str,
+                              symbol_table: Optional[SymbolTable] = None):
         '''Generate the PSyIR tree for the supplied Fortran statement. The
         symbol table is expected to provide all symbols found in the
         expression.
 
-        :param str source_code: text of the expression to be parsed.
+        :param source_code: text of the expression to be parsed.
         :param symbol_table: the SymbolTable in which to search for any
             symbols that are encountered.
-        :type symbol_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
 
         :returns: PSyIR representing the provided Fortran expression.
         :rtype: :py:class:`psyclone.psyir.nodes.Node`
@@ -118,7 +119,9 @@ class FortranReader():
             Fortran expression.
 
         '''
-        if not isinstance(symbol_table, SymbolTable):
+        if symbol_table is None:
+            symbol_table = SymbolTable()
+        elif not isinstance(symbol_table, SymbolTable):
             raise TypeError(f"Must be supplied with a valid SymbolTable but "
                             f"got '{type(symbol_table).__name__}'")
 
@@ -142,15 +145,15 @@ class FortranReader():
         self._processor.process_nodes(fake_parent[0], [parse_tree])
         return fake_parent[0].children[0].detach()
 
-    def psyir_from_statement(self, source_code, symbol_table):
+    def psyir_from_statement(self, source_code: str,
+                             symbol_table: Optional[SymbolTable] = None):
         '''Generate the PSyIR tree for the supplied Fortran statement. The
         symbolt table is expected to provide all symbols found in the
         statement.
 
-        :param str source_code: text of the statement to be parsed.
+        :param source_code: text of the statement to be parsed.
         :param symbol_table: the SymbolTable in which to search for any
             symbols that are encountered.
-        :type symbol_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
 
         :returns: PSyIR representing the provided Fortran statement.
         :rtype: :py:class:`psyclone.psyir.nodes.Node`
@@ -160,7 +163,9 @@ class FortranReader():
             Fortran statement.
 
         '''
-        if not isinstance(symbol_table, SymbolTable):
+        if symbol_table is None:
+            symbol_table = SymbolTable()
+        elif not isinstance(symbol_table, SymbolTable):
             raise TypeError(f"Must be supplied with a valid SymbolTable but "
                             f"got '{type(symbol_table).__name__}'")
         string_reader = FortranStringReader(source_code)
diff --git a/src/psyclone/psyir/nodes/array_mixin.py b/src/psyclone/psyir/nodes/array_mixin.py
index eaec20b55..d1ccab239 100644
--- a/src/psyclone/psyir/nodes/array_mixin.py
+++ b/src/psyclone/psyir/nodes/array_mixin.py
@@ -133,8 +133,8 @@ class ArrayMixin(metaclass=abc.ABCMeta):
         :type expr: :py:class:`psyclone.psyir.nodes.Node`
         :param bound_operator: the particular bound operation.
         :type bound_operator:
-            :py:class:`psyclone.psyir.nodes.IntrinsicCall.Intrinsic.LBOUND |
-            :py:class:`psyclone.psyir.nodes.IntrinsicCall.Intrinsic.UBOUND
+            :py:class:`psyclone.psyir.nodes.IntrinsicCall.Intrinsic.LBOUND` |
+            :py:class:`psyclone.psyir.nodes.IntrinsicCall.Intrinsic.UBOUND`
         :param int index: the bounds index.
 
         :returns: True if the expr is in the expected form and False otherwise.
@@ -598,6 +598,126 @@ class ArrayMixin(metaclass=abc.ABCMeta):
                 return child.position
         raise IndexError
 
+    def same_range(self, index: int, array2, index2: int) -> bool:
+        ''' This method compares the range of this array node at a given index
+        with the range of a second array at a second index. This is useful to
+        verify if array operations are valid, e.g.: A(3,:,5) + B(:,2,2).
+
+        Note that this check supports symbolic comparisons, e.g.:
+        A(3:4) has the same range as B(2+1:5-1),
+        and will consider compile-time unknown dimensions as equal, e.g.:
+        A(:) has the same range as B(:).
+
+        TODO #2485. This method has false negatives: cases when the range
+        is the same but it can not be proved, so we return False.
+
+        TODO #2004. This method currently compares exact ranges, not just the
+        length of them, which could be done with "(upper-lower)/step" symbolic
+        comparisons. This is because arrayrange2loop does not account for
+        arrays declared with different lbounds, but this could be improved.
+
+        :param index: the index indicating the location of a range node in
+            this array.
+        :param array2: the array accessor that we want to compare it to.
+        :param index2: the index indicating the location of a range node in
+            array2.
+
+        :returns: True if the ranges are the same and False if they are not
+            the same, or if it is not possible to determine.
+
+        :raises: TypeError if any of the arguments are of the wrong type.
+
+        '''
+        # pylint: disable=too-many-branches
+        if not isinstance(index, int):
+            raise TypeError(
+                f"The 'index' argument of the same_range() method should be an"
+                f" int but found '{type(index).__name__}'.")
+        if not isinstance(array2, ArrayMixin):
+            raise TypeError(
+                f"The 'array2' argument of the same_range() method should be "
+                f"an ArrayMixin but found '{type(array2).__name__}'.")
+        if not isinstance(index2, int):
+            raise TypeError(
+                f"The 'index2' argument of the same_range() method should be "
+                f"an int but found '{type(index2).__name__}'.")
+        if not index < len(self.children):
+            raise IndexError(
+                f"The value of the 'index' argument of the same_range() method"
+                f" is '{index}', but it should be less than the number of "
+                f"dimensions in the associated array, which is "
+                f"'{len(self.children)}'.")
+        if not index2 < len(array2.children):
+            raise IndexError(
+                f"The value of the 'index2' argument of the same_range() "
+                f"method is '{index2}', but it should be less than the number"
+                f" of dimensions in the associated array 'array2', which is "
+                f"'{len(array2.children)}'.")
+        if not isinstance(self.children[index], Range):
+            raise TypeError(
+                f"The child of the first array argument at the specified index"
+                f" '{index}' should be a Range node, but found "
+                f"'{type(self.children[index]).__name__}'.")
+        if not isinstance(array2.children[index2], Range):
+            raise TypeError(
+                f"The child of the second array argument at the specified "
+                f"index '{index2}' should be a Range node, but found "
+                f"'{type(array2.children[index2]).__name__}'.")
+
+        range1 = self.children[index]
+        range2 = array2.children[index2]
+
+        sym_maths = SymbolicMaths.get()
+        # compare lower bounds
+        if self.is_lower_bound(index) and array2.is_lower_bound(index2):
+            # Both self and array2 use the lbound() intrinsic to
+            # specify the lower bound of the array dimension. We may
+            # not be able to determine what the lower bounds of these
+            # arrays are statically but at runtime the code will fail
+            # if the ranges do not match so we assume that the lower
+            # bounds are consistent.
+            pass
+        elif self.is_lower_bound(index) or array2.is_lower_bound(index2):
+            # One and only one of self and array2 use the lbound()
+            # intrinsic to specify the lower bound of the array
+            # dimension. In this case assume that the ranges are
+            # different (although they could potentially be the same).
+            return False
+        elif not sym_maths.equal(range1.start, range2.start):
+            # Neither self nor array2 use the lbound() intrinsic to
+            # specify the lower bound of the array dimension. Try to
+            # determine if they are the same by matching the
+            # text. Use symbolic maths to do the comparison.
+            return False
+
+        # compare upper bounds
+        if self.is_upper_bound(index) and array2.is_upper_bound(index2):
+            # Both self and array2 use the ubound() intrinsic to
+            # specify the upper bound of the array dimension. We may
+            # not be able to determine what the upper bounds of these
+            # arrays are statically but at runtime the code will fail
+            # if the ranges do not match so we assume that the upper
+            # bounds are consistent.
+            pass
+        elif self.is_upper_bound(index) or array2.is_upper_bound(index2):
+            # One and only one of self and array2 use the ubound()
+            # intrinsic to specify the upper bound of the array
+            # dimension. In this case assume that the ranges are
+            # different (although they could potentially be the same).
+            return False
+        elif not sym_maths.equal(range1.stop, range2.stop):
+            # Neither self nor array2 use the ubound() intrinsic to
+            # specify the upper bound of the array dimension. Use
+            # symbolic maths to check if they are equal.
+            return False
+
+        # compare steps
+        if not sym_maths.equal(range1.step, range2.step):
+            return False
+
+        # Everything matches.
+        return True
+
 
 # For AutoAPI documentation generation
 __all__ = ['ArrayMixin']
diff --git a/src/psyclone/psyir/transformations/arrayrange2loop_trans.py b/src/psyclone/psyir/transformations/arrayrange2loop_trans.py
index 05fae640c..6ad3aa121 100644
--- a/src/psyclone/psyir/transformations/arrayrange2loop_trans.py
+++ b/src/psyclone/psyir/transformations/arrayrange2loop_trans.py
@@ -41,7 +41,6 @@ not support array ranges.
 
 '''
 
-from psyclone.core import SymbolicMaths
 from psyclone.psyGen import Transformation
 from psyclone.psyir.nodes import Loop, Range, Reference, ArrayReference, \
     Assignment, Call, IntrinsicCall
@@ -78,127 +77,6 @@ class ArrayRange2LoopTrans(Transformation):
 
     '''
 
-    @staticmethod
-    def same_range(array1, idx1, array2, idx2):
-        '''This method compares the range node at position 'idx1' in array
-        access 'array1' with the range node at position 'idx2' in
-        array access 'array2'.
-
-        The natural place to test the equivalence of two ranges is in
-        the Range class. However, the test required here is slightly
-        different as it is valid to assume that two array slices are
-        the same even if their actual sizes are not known (as the code
-        would raise a runtime exception if this were not the case).
-
-        :param array1: an array node containing a range node at index idx1.
-        :type array1: py:class:`psyclone.psyir.node.ArrayReference`
-        :param int idx1: an index indicating the location of a range \
-            node in array1 (in its children list).
-        :param array2: an array node containing a range node at index idx2.
-        :type array2: py:class:`psyclone.psyir.node.ArrayReference`
-        :param int idx2: an index indicating the location of a range \
-            node in array2 (in its children list).
-
-        :returns: True if the ranges are the same and False if they \
-            are not the same, or if it is not possible to determine.
-        :rtype: bool
-
-        :raises: TypeError if one or more of the arguments are of the \
-            wrong type.
-
-        '''
-        # pylint: disable=too-many-branches
-        if not isinstance(array1, ArrayReference):
-            raise TypeError(
-                f"The first argument to the same_range() method should be an "
-                f"ArrayReference but found '{type(array1).__name__}'.")
-        if not isinstance(idx1, int):
-            raise TypeError(
-                f"The second argument to the same_range() method should be an "
-                f"int but found '{type(idx1).__name__}'.")
-        if not isinstance(array2, ArrayReference):
-            raise TypeError(
-                f"The third argument to the same_range() method should be an "
-                f"ArrayReference but found '{type(array2).__name__}'.")
-        if not isinstance(idx2, int):
-            raise TypeError(
-                f"The fourth argument to the same_range() method should be an "
-                f"int but found '{type(idx2).__name__}'.")
-        if not idx1 < len(array1.children):
-            raise IndexError(
-                f"The value of the second argument to the same_range() method "
-                f"'{idx1}' should be less than the number of dimensions "
-                f"'{len(array1.children)}' in the associated array 'array1'.")
-        if not idx2 < len(array2.children):
-            raise IndexError(
-                f"The value of the fourth argument to the same_range() method "
-                f"'{idx2}' should be less than the number of dimensions "
-                f"'{len(array2.children)}' in the associated array 'array2'.")
-        if not isinstance(array1.children[idx1], Range):
-            raise TypeError(
-                f"The child of the first array argument at the specified index"
-                f" ({idx1}) should be a Range node, but found "
-                f"'{type(array1.children[idx1]).__name__}'.")
-        if not isinstance(array2.children[idx2], Range):
-            raise TypeError(
-                f"The child of the second array argument at the specified "
-                f"index ({idx2}) should be a Range node, but found "
-                f"'{type(array2.children[idx2]).__name__}'.")
-
-        range1 = array1.children[idx1]
-        range2 = array2.children[idx2]
-
-        sym_maths = SymbolicMaths.get()
-        # compare lower bounds
-        if array1.is_lower_bound(idx1) and array2.is_lower_bound(idx2):
-            # Both array1 and array2 use the lbound() intrinsic to
-            # specify the lower bound of the array dimension. We may
-            # not be able to determine what the lower bounds of these
-            # arrays are statically but at runtime the code will fail
-            # if the ranges do not match so we assume that the lower
-            # bounds are consistent.
-            pass
-        elif array1.is_lower_bound(idx1) or array2.is_lower_bound(idx2):
-            # One and only one of array1 and array2 use the lbound()
-            # intrinsic to specify the lower bound of the array
-            # dimension. In this case assume that the ranges are
-            # different (although they could potentially be the same).
-            return False
-        elif not sym_maths.equal(range1.start, range2.start):
-            # Neither array1 nor array2 use the lbound() intrinsic to
-            # specify the lower bound of the array dimension. Try to
-            # determine if they are the same by matching the
-            # text. Use symbolic maths to do the comparison.
-            return False
-
-        # compare upper bounds
-        if array1.is_upper_bound(idx1) and array2.is_upper_bound(idx2):
-            # Both array1 and array2 use the ubound() intrinsic to
-            # specify the upper bound of the array dimension. We may
-            # not be able to determine what the upper bounds of these
-            # arrays are statically but at runtime the code will fail
-            # if the ranges do not match so we assume that the upper
-            # bounds are consistent.
-            pass
-        elif array1.is_upper_bound(idx1) or array2.is_upper_bound(idx2):
-            # One and only one of array1 and array2 use the ubound()
-            # intrinsic to specify the upper bound of the array
-            # dimension. In this case assume that the ranges are
-            # different (although they could potentially be the same).
-            return False
-        elif not sym_maths.equal(range1.stop, range2.stop):
-            # Neither array1 nor array2 use the ubound() intrinsic to
-            # specify the upper bound of the array dimension. Use
-            # symbolic maths to check if they are equal.
-            return False
-
-        # compare steps
-        if not sym_maths.equal(range1.step, range2.step):
-            return False
-
-        # Everything matches.
-        return True
-
     def apply(self, node, options=None):
         '''Apply the ArrayRange2Loop transformation to the specified node. The
         node must be an assignment. The rightmost range node in each array
@@ -322,8 +200,7 @@ class ArrayRange2LoopTrans(Transformation):
                     # loop variables where the ranges are
                     # different, or occur in different index
                     # locations.
-                    if not ArrayRange2LoopTrans.same_range(
-                            node.lhs, lhs_index, array, idx):
+                    if not node.lhs.same_range(lhs_index, array, idx):
                         # Ranges are, or may be, different so we
                         # can't safely replace this range with a
                         # loop iterator.


Test patch:
diff --git a/src/psyclone/tests/psyir/frontend/fortran_test.py b/src/psyclone/tests/psyir/frontend/fortran_test.py
index d31e53d35..3ec9f57d4 100644
--- a/src/psyclone/tests/psyir/frontend/fortran_test.py
+++ b/src/psyclone/tests/psyir/frontend/fortran_test.py
@@ -149,29 +149,24 @@ def test_fortran_psyir_from_expression_invalid(fortran_reader):
     error when given something that is not an expression. '''
     # No supplied table
     with pytest.raises(TypeError) as err:
-        fortran_reader.psyir_from_expression("3.0 + sin(a)", None)
-    assert ("Must be supplied with a valid SymbolTable but got 'NoneType'" in
+        fortran_reader.psyir_from_expression("3.0 + sin(a)", 3)
+    assert ("Must be supplied with a valid SymbolTable but got 'int'" in
             str(err.value))
     with pytest.raises(TypeError) as err:
         fortran_reader.psyir_from_expression("3.0 + sin(a)", "wrong")
     assert ("Must be supplied with a valid SymbolTable but got 'str'" in
             str(err.value))
-    with pytest.raises(TypeError) as err:
-        fortran_reader.psyir_from_expression("3.0 + sin(a)", None)
-    assert ("Must be supplied with a valid SymbolTable but got 'NoneType'" in
-            str(err.value))
-    table = SymbolTable()
     # OK
-    fortran_reader.psyir_from_expression("return", table)
+    fortran_reader.psyir_from_expression("return")
     with pytest.raises(ValueError) as err:
-        fortran_reader.psyir_from_expression("a = b", table)
+        fortran_reader.psyir_from_expression("a = b")
     assert "not represent a Fortran expression: 'a = b'" in str(err.value)
     with pytest.raises(ValueError) as err:
-        fortran_reader.psyir_from_expression("if(3 == 2)then", table)
+        fortran_reader.psyir_from_expression("if(3 == 2)then")
     assert ("not represent a Fortran expression: 'if(3 == 2)then'" in
             str(err.value))
     with pytest.raises(ValueError) as err:
-        fortran_reader.psyir_from_expression("this is not Fortran", table)
+        fortran_reader.psyir_from_expression("this is not Fortran")
     assert ("not represent a Fortran expression: 'this is not Fortran'" in
             str(err.value))
 
@@ -192,16 +187,15 @@ def test_psyir_from_statement_invalid(fortran_reader):
     ''' Test that the psyir_from_statement method raises the expected error
     when given something that is not a statement. '''
     with pytest.raises(TypeError) as err:
-        fortran_reader.psyir_from_statement("blah", None)
-    assert ("Must be supplied with a valid SymbolTable but got 'NoneType'"
+        fortran_reader.psyir_from_statement("blah", 3)
+    assert ("Must be supplied with a valid SymbolTable but got 'int'"
             in str(err.value))
-    table = SymbolTable()
     with pytest.raises(ValueError) as err:
-        fortran_reader.psyir_from_statement("blah", table)
+        fortran_reader.psyir_from_statement("blah")
     assert ("Supplied source does not represent a Fortran statement: 'blah'"
             in str(err.value))
     # OK
-    fortran_reader.psyir_from_statement("a=b", table)
+    fortran_reader.psyir_from_statement("a=b")
 
 
 def test_fortran_psyir_from_file(fortran_reader, tmpdir_factory):
diff --git a/src/psyclone/tests/psyir/nodes/array_mixin_test.py b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
index 99cbdc5e0..afade7093 100644
--- a/src/psyclone/tests/psyir/nodes/array_mixin_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
@@ -564,3 +564,132 @@ def test_get_outer_range_index_error():
     array = ArrayReference.create(symbol, [Literal("2", INTEGER_TYPE)])
     with pytest.raises(IndexError):
         _ = array.get_outer_range_index()
+
+
+def test_same_range_error(fortran_reader):
+    ''' Test that the same_range method produces the expected errors. '''
+
+    array1 = fortran_reader.psyir_from_statement("a(i) = 0").lhs
+    array2 = fortran_reader.psyir_from_statement("b(j) = 0").lhs
+
+    with pytest.raises(TypeError) as info:
+        array1.same_range(None, None, None)
+    assert ("The 'index' argument of the same_range() method should be an "
+            "int but found 'NoneType'." in str(info.value))
+
+    with pytest.raises(TypeError) as info:
+        array1.same_range(1, None, None)
+    assert ("The 'array2' argument of the same_range() method should be an "
+            "ArrayMixin but found 'NoneType'." in str(info.value))
+
+    with pytest.raises(TypeError) as info:
+        array1.same_range(1, array2, None)
+    assert ("The 'index2' argument of the same_range() method should be an "
+            "int but found 'NoneType'." in str(info.value))
+
+    with pytest.raises(IndexError) as info:
+        array1.same_range(1, array2, 2)
+    assert ("The value of the 'index' argument of the same_range() method "
+            "is '1', but it should be less than the number of dimensions "
+            "in the associated array, which is '1'." in str(info.value))
+
+    with pytest.raises(IndexError) as info:
+        array1.same_range(0, array2, 2)
+    assert ("The value of the 'index2' argument of the same_range() method "
+            "is '2', but it should be less than the number of dimensions in "
+            "the associated array 'array2', which is '1'." in str(info.value))
+
+    with pytest.raises(TypeError) as info:
+        array1.same_range(0, array2, 0)
+    assert ("The child of the first array argument at the specified index '0' "
+            "should be a Range node, but found 'Reference'" in str(info.value))
+
+    array1 = fortran_reader.psyir_from_statement("a(:) = 0").lhs
+
+    with pytest.raises(TypeError) as info:
+        array1.same_range(0, array2, 0)
+    assert ("The child of the second array argument at the specified index "
+            "'0' should be a Range node, but found 'Reference'"
+            in str(info.value))
+
+
+def test_same_range(fortran_reader):
+    '''Test that the same_range method produces the expected results. '''
+
+    array2 = fortran_reader.psyir_from_statement("b(:) = 0").lhs
+    array1 = fortran_reader.psyir_from_statement("a(:) = 0").lhs
+
+    # lower bounds both use lbound, upper bounds both use ubound and
+    # step is the same so everything matches.
+    assert array1.same_range(0, array2, 0) is True
+
+    # Check if steps are different
+    array2.children[0].step = Literal("2", INTEGER_TYPE)
+    assert array1.same_range(0, array2, 0) is False
+
+    # one of upper bounds uses ubound, other does not
+    array2 = fortran_reader.psyir_from_statement("b(:4) = 0").lhs
+    assert array1.same_range(0, array2, 0) is False
+
+    # both have an specific ubound and are different
+    array1 = fortran_reader.psyir_from_statement("a(:3) = 0").lhs
+    assert array1.same_range(0, array2, 0) is False
+
+    # one of lower bounds uses lbound, other does not
+    array2 = fortran_reader.psyir_from_statement("b(2:) = 0").lhs
+    assert array1.same_range(0, array2, 0) is False
+
+    # both have an specific lbound and are different
+    array1 = fortran_reader.psyir_from_statement("a(3:) = 0").lhs
+    assert array1.same_range(0, array2, 0) is False
+
+    # Both have lower and upper bound but are symbolically equal
+    array1 = fortran_reader.psyir_from_statement("a(3:5) = 0").lhs
+    array2 = fortran_reader.psyir_from_statement("a(2+1:6-1) = 0").lhs
+    assert array1.same_range(0, array2, 0) is True
+
+    # One uses bound and the other does not, but the compile time known
+    # shape is the same
+    code = '''
+    subroutine test()
+        real, dimension(4, 5-1, 2:5) :: A
+        real, dimension(4,   4,   4) :: B
+        A(:,:,:) = B(1:4,1:4, 1:4)
+    end subroutine
+    '''
+    psyir = fortran_reader.psyir_from_source(code)
+    array1, array2 = psyir.walk(Assignment)[0].children
+    assert array1.same_range(0, array2, 0) is True
+    assert array1.same_range(1, array2, 1) is True
+    assert array1.same_range(2, array2, 2) is True
+
+    # The asserts below are incorrect
+    pytest.xfail("issue #2485: same_range still has problems")
+
+    # Full-ranges but the compile time known shape is not the same
+    code = '''
+    subroutine test()
+        real, dimension(4, 5-1, 2:5) :: A
+        real, dimension(5,   5,   5) :: B
+        A(:,:,:) = B(:,:,:)
+    end subroutine
+    '''
+    psyir = fortran_reader.psyir_from_source(code)
+    array1, array2 = psyir.walk(Assignment)[0].children
+    assert array1.same_range(0, array2, 0) is False
+    assert array1.same_range(1, array2, 1) is False
+    assert array1.same_range(2, array2, 2) is False
+
+    # Same range length, but shifted
+    code = '''
+    subroutine test()
+        real, dimension(4, 5-1, 2:5) :: A
+        real, dimension(5,   5,   5) :: B
+        A(2:4,2:4,2:4) = B(3:5,3:5,3:5)
+    end subroutine
+    '''
+    psyir = fortran_reader.psyir_from_source(code)
+    array1, array2 = psyir.walk(Assignment)[0].children
+    assert array1.same_range(0, array2, 0) is True
+    assert array1.same_range(1, array2, 1) is True
+    assert array1.same_range(2, array2, 2) is True
diff --git a/src/psyclone/tests/psyir/transformations/arrayrange2loop_trans_test.py b/src/psyclone/tests/psyir/transformations/arrayrange2loop_trans_test.py
index 5fa08b34a..e4d162cea 100644
--- a/src/psyclone/tests/psyir/transformations/arrayrange2loop_trans_test.py
+++ b/src/psyclone/tests/psyir/transformations/arrayrange2loop_trans_test.py
@@ -268,104 +268,6 @@ def test_transform():
     assert isinstance(ArrayRange2LoopTrans(), Transformation)
 
 
-def test_same_range():
-    '''Test that the same_range utility function behaves in the expected
-    way.
-
-    '''
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(None, None, None, None)
-    assert ("The first argument to the same_range() method should be an "
-            "ArrayReference but found 'NoneType'." in str(info.value))
-
-    array_type = ArrayType(REAL_TYPE, [10])
-    array_value = ArrayReference.create(
-        DataSymbol("dummy", array_type), [DataNode("x")])
-    array_range = ArrayReference.create(
-        DataSymbol("dummy", array_type), [Range()])
-
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, None, None, None)
-    assert ("The second argument to the same_range() method should be an "
-            "int but found 'NoneType'." in str(info.value))
-
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, 1, None, None)
-    assert ("The third argument to the same_range() method should be an "
-            "ArrayReference but found 'NoneType'." in str(info.value))
-
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, 1, array_value, None)
-    assert ("The fourth argument to the same_range() method should be an "
-            "int but found 'NoneType'." in str(info.value))
-
-    with pytest.raises(IndexError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, 1, array_value, 2)
-    assert ("The value of the second argument to the same_range() method "
-            "'1' should be less than the number of dimensions '1' in the "
-            "associated array 'array1'." in str(info.value))
-
-    with pytest.raises(IndexError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, 0, array_value, 2)
-    assert ("The value of the fourth argument to the same_range() method "
-            "'2' should be less than the number of dimensions '1' in the "
-            "associated array 'array2'." in str(info.value))
-
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(array_value, 0, array_value, 0)
-    assert ("The child of the first array argument at the specified index (0) "
-            "should be a Range node, but found 'DataNode'" in str(info.value))
-
-    with pytest.raises(TypeError) as info:
-        ArrayRange2LoopTrans.same_range(array_range, 0, array_value, 0)
-    assert ("The child of the second array argument at the specified index "
-            "(0) should be a Range node, but found 'DataNode'"
-            in str(info.value))
-
-    # lower bounds both use lbound, upper bounds both use ubound and
-    # step is the same so everything matches.
-    array_x = create_array_x(SymbolTable())
-    array_x_2 = create_array_x(SymbolTable())
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is True
-
-    # steps are different
-    tmp = array_x_2.children[0].step
-    array_x_2.children[0].step = Literal("2", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is False
-
-    # Put step value back to what it was in case it affects the ubound
-    # and lbound tests
-    array_x_2.children[0].step = tmp
-
-    # one of upper bounds uses ubound, other does not
-    tmp1 = array_x_2.children[0].stop
-    array_x_2.children[0].stop = Literal("2", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is False
-
-    # neither use upper bound and are different
-    tmp2 = array_x.children[0].stop
-    array_x.children[0].stop = Literal("1", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is False
-
-    # Put upper bounds back to what they were in case they affect the
-    # lbound tests
-    array_x_2.children[0].stop = tmp1
-    array_x.children[0].stop = tmp2
-
-    # One of lower bounds uses lbound, other does not but has the same value.
-    array_x_2.children[0].start = Literal("1", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is True
-
-    # One of lower bounds uses lbound, other does not and has a
-    # different start value.
-    array_x_2.children[0].start = Literal("2", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is False
-
-    # Neither use lower bound and are different.
-    array_x.children[0].start = Literal("3", INTEGER_TYPE)
-    assert ArrayRange2LoopTrans.same_range(array_x, 0, array_x_2, 0) is False
-
-
 # The parameterised tests are 1: x(:)=0.0, 2: x(:)=y(n,:), 3:
 # y(n,:)=x(:), 4: y2(:,:)=z(:,n,:) and 5:
 # y3(n,2:n:2)=x(2:n:2)*z(1,2:n:2)+a(1)