Repository: stfc/PSyclone

Issue:
[PSyData] Minor extraction library updates
With the kernel extraction started to be used more (e.g. in NG-ARCH), I am in the process of integration this with our Baf build system (which is the lfric Fab build system). That will involve copying the PSyclone NetCDF LFRic extraction files into the build system (so they can be injected into the Fab build, so no need for creating libraries etc). A few changes required:
- [ ] Minor updates to debug output (using `trim` to remove white spaces)
- [ ] LFRic's real field types have been renamed (`field_r{{prec}}_mod` to `field_real{{prec}}_mod` etc). We can't simply switch this, since atm our infrastructure is outdated (see #2623 for details, and also #2239 - best solution is to switch to the now stand-alone lfric infrastructure files, which should support non-MPI builds now). As a quick solution till this is properly fixed, I'll add a second copy of the jinja files with the new names.
can_loop_be_parallelised fails if underlying SymbolicMath comparison produce a TypeError
NEMOv5 (-r ORCA2_ICE_PISCES) [mppini.f90](https://forge.nemo-ocean.eu/nemo/nemo/-/blob/main/src/OCE/LBC/mppini.F90)

fails in the `loop_directive_trans.apply(loop)` because it calls the dependency tools `dtools.can_loop_be_parallelised` and this does a symbolic comparison that fails with:
```
  File "psyclone/core/symbolic_maths.py", line 203, in _subtract
    return simplify(sympy_expressions[0] - sympy_expressions[1])
                    ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for -: 'One' and 'tuple'
```


Discussion:

Patch:
diff --git a/changelog b/changelog
index 95be46451..5c4f0c036 100644
--- a/changelog
+++ b/changelog
@@ -50,6 +50,9 @@
 	17) PR #2829 for #2826. Adds a --config command-line argument to the
 	PSyAD script (to replicate that for psyclone).
 
+	18) PR #2280 for #2723. Add support for depencency analysis involving
+	array ranges.
+
 release 3.0.0 6th of December 2024
 
 	1) PR #2477 for #2463. Add support for Fortran Namelist statements.
diff --git a/src/psyclone/psyir/tools/dependency_tools.py b/src/psyclone/psyir/tools/dependency_tools.py
index e29e4b87a..c0380d281 100644
--- a/src/psyclone/psyir/tools/dependency_tools.py
+++ b/src/psyclone/psyir/tools/dependency_tools.py
@@ -48,7 +48,7 @@ from psyclone.core import (AccessType, Signature, SymbolicMaths,
 from psyclone.errors import InternalError, LazyString
 from psyclone.psyir.backend.sympy_writer import SymPyWriter
 from psyclone.psyir.backend.visitor import VisitorError
-from psyclone.psyir.nodes import Loop
+from psyclone.psyir.nodes import Loop, Node, Range
 
 
 # pylint: disable=too-many-lines
@@ -281,6 +281,50 @@ class DependencyTools():
 
         return partition_infos
 
+    # -------------------------------------------------------------------------
+    @staticmethod
+    def _ranges_overlap(range1: Node,
+                        range2: Node) -> bool:
+        '''This function tests if two ranges overlap. It also accepts a simple
+        index as 'range' (e.g. just `i`), which will be converted into `i:i:1`
+        before comparing. At this stage, this function simple checks if one of
+        the ranges starts after the other (e.g. 1:3, and 5:7). It will handle
+        unspecified ranges (":"), and will report an overlap.
+        Additional tests e.g. using the step value are not yet implemented
+        (e.g. 1:10:2 and 2:10:2 will not overlap, but this will not be
+        detected atm).
+
+        :param range1: The first range or expression.
+        :param range2: The second range or expression.
+
+        :returns: whether the ranges (or an index expression with a range)
+            overlap or not
+
+        '''
+        if not isinstance(range1, Range):
+            # Not a range, must be some index `i`. Create a range `i:i:1`
+            range1 = Range.create(range1.copy(), range1.copy())
+        if not isinstance(range2, Range):
+            # Not a range, must be some index `i`. Create a range `i:i:1`
+            range2 = Range.create(range2.copy(), range2.copy())
+
+        sm = SymbolicMaths.get()
+
+        # Check if the first range is smaller than the second one, e.g.:
+        # 1:3:1 and 4:6:1
+        if sm.greater_than(range2.start, range1.stop) == sm.Fuzzy.TRUE:
+            # The first range is before the second range, so no overlap
+            return False
+        # Check if the second range is smaller than the first one, e.g.:
+        # 4:6:1 and 1:3:1
+        if sm.greater_than(range1.start, range2.stop) == sm.Fuzzy.TRUE:
+            # The second range is before the first range, so no overlap
+            return False
+
+        # We could do additional tests here, e.g. including step to determine
+        # that 1:10:2 does not overlap with 2:10:2
+        return True
+
     # -------------------------------------------------------------------------
     @staticmethod
     def _independent_0_var(index_exp1, index_exp2):
@@ -296,6 +340,9 @@ class DependencyTools():
         :type index_exp2: :py:class:`psyclone.psyir.nodes.Node`
 
         '''
+        if isinstance(index_exp1, Range) or isinstance(index_exp2, Range):
+            return not DependencyTools._ranges_overlap(index_exp1, index_exp2)
+
         sym_maths = SymbolicMaths.get()
 
         # If the indices can be shown to be never equal, the accesses


Test patch:
diff --git a/src/psyclone/tests/psyir/tools/dependency_tools_test.py b/src/psyclone/tests/psyir/tools/dependency_tools_test.py
index 8c4e28995..a0eff77f4 100644
--- a/src/psyclone/tests/psyir/tools/dependency_tools_test.py
+++ b/src/psyclone/tests/psyir/tools/dependency_tools_test.py
@@ -41,7 +41,7 @@ import pytest
 from psyclone.configuration import Config
 from psyclone.core import AccessType, Signature, VariablesAccessInfo
 from psyclone.errors import InternalError
-from psyclone.psyir.nodes import Loop
+from psyclone.psyir.nodes import Assignment, Loop
 from psyclone.psyir.tools import DependencyTools, DTCode
 from psyclone.tests.utilities import get_invoke
 
@@ -1107,3 +1107,67 @@ def test_fuse_dimension_change(fortran_reader):
             "in different index locations: s%comp1(jj)%comp2(ji) and "
             "s%comp1(ji)%comp2(jj)."
             in str(msg))
+
+
+# ----------------------------------------------------------------------------
+@pytest.mark.parametrize("range1, range2, overlap",
+                         [("1:3", "4:6", False),
+                          ("3:9", "-1:-3", False),
+                          ("1:3", "4", False),
+                          ("5", "-1:-3", False),
+                          ("i:i+3", "i+5:i+7", False),
+                          ("i:i+3", "i+2", True),
+                          ("i:i+3", "i+5", False),
+                          ("i:i+3", "i-1", False),
+                          (":", "1", True),
+                          (":", "i", True),
+                          ("::", "1", True),
+                          ("::", "i", True),
+                          ("1", ":", True),
+                          ("i", ":", True),
+                          ])
+def test_ranges_overlap(range1, range2, overlap, fortran_reader):
+    '''Test the detection of overlapping ranges.
+    '''
+    source = f'''program test
+                 integer i, ji, inbj
+                 integer, parameter :: jpi=5, jpj=10
+                 real, dimension(jpi,jpi) :: ldisoce
+
+                 ldisoce({range1},{range2}) = 1.0
+                 end program test'''
+
+    psyir = fortran_reader.psyir_from_source(source)
+    dep_tools = DependencyTools()
+    assign = psyir.walk(Assignment)[0]
+    r1 = assign.lhs.children[0]
+    r2 = assign.lhs.children[1]
+    assert dep_tools._ranges_overlap(r1, r2) == overlap
+    # Also make sure that _independent_0_var handles this correctly:
+    assert dep_tools._independent_0_var(r1, r2) is not overlap
+
+
+# ----------------------------------------------------------------------------
+def test_nemo_example_ranges(fortran_reader):
+    '''Tests an actual NEMO example
+    '''
+    source = '''program test
+                integer ji, inbj
+                integer, parameter :: jpi=5, jpj=10
+                real, dimension(jpi,jpi) :: ldisoce
+                do jj = 1, inbj, 1
+                  if (COUNT(ldisoce(:,jj)) == 0) then
+                    ldisoce(1,jj) = .true.
+                  end if
+                enddo
+                end program test'''
+
+    psyir = fortran_reader.psyir_from_source(source)
+    loops = psyir.children[0].children[0]
+    dep_tools = DependencyTools()
+
+    # This loop can be parallelised because all instances of ldisoce use
+    # the index jj in position 2 (the overlap between ":" and "1"
+    # is tested in test_ranges_overlap above, here we check that this
+    # overlap is indeed ignored because of the jj index).
+    assert dep_tools.can_loop_be_parallelised(loops)