Repository: stfc/PSyclone (python)

Issue:
Add support to parse Fortran array bounds with expressions
For example: `real :: a(start - 1: stop + 1)`

Currently it is a UnknownFortranType, but it should be an ArrayType with an ArrayBound with 2 psyir expressions.

Pull request description:
(Closes #2138) add support for expressions in array bound definitions

Patch:
diff --git a/changelog b/changelog
index 2f24ee883..ee36b4868 100644
--- a/changelog
+++ b/changelog
@@ -160,8 +160,11 @@
 	PSyData API (e.g. when profiling).
 
 	57) PR #2647 for #1992. Adds MPI support to PSyKE. Each rank will write
-        its own output file(s). The generated driver has been extended so that
-        the name of the file to use can be specified on the command line.
+    its own output file(s). The generated driver has been extended so that
+    the name of the file to use can be specified on the command line.
+
+	58) PR #2637 for #2138. Add Fortran frontend support to parser array
+	declarations with expressions in their shape dimensions.
 
 release 2.5.0 14th of February 2024
 
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index 4b15ed65b..e03a85fa4 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -66,7 +66,7 @@ from psyclone.psyir.symbols import (
     CommonBlockInterface, ContainerSymbol, DataSymbol, DataTypeSymbol,
     DefaultModuleInterface, GenericInterfaceSymbol, ImportInterface,
     INTEGER_TYPE, NoType, RoutineSymbol, ScalarType, StaticInterface,
-    StructureType, Symbol, SymbolError, SymbolTable, UnknownInterface,
+    StructureType, Symbol, SymbolError, UnknownInterface,
     UnresolvedInterface, UnresolvedType, UnsupportedFortranType,
     UnsupportedType)
 
@@ -315,7 +315,7 @@ def _find_or_create_unresolved_symbol(location, name, scope_limit=None,
     # tree has not been built so the symbol table is not connected to
     # a node.
     symbol_table = location.scope.symbol_table
-    while symbol_table.node and not isinstance(
+    while symbol_table and symbol_table.node and not isinstance(
             symbol_table.node, (Routine, Container)):
         symbol_table = symbol_table.parent_symbol_table()
 
@@ -1302,93 +1302,52 @@ class Fparser2Reader():
 
         return selected_routines
 
-    @staticmethod
-    def _parse_dimensions(dimensions, symbol_table):
+    def _process_array_bound(self, bound_expr, table):
+        '''Process the supplied fparser2 parse tree for the upper/lower
+        bound of a dimension in an array declaration.
+
+        :param bound_expr: fparser2 parse tree for lower/upper bound.
+        :type bound_expr: :py:class:`fparser.two.utils.Base`
+
+        :returns: PSyIR for the bound.
+        :rtype: :py:class:`psyclone.psyir.nodes.DataNode`
+
+        '''
+        dummy = Assignment(parent=table.node)
+        self.process_nodes(parent=dummy, nodes=[bound_expr])
+
+        return dummy.children[0].detach()
+
+    def _parse_dimensions(self, dimensions, symbol_table):
         '''
         Parse the fparser dimension attribute into a shape list. Each entry of
         this list is either None (if the extent is unknown) or a 2-tuple
-        containing the lower and upper bound of that dimension. If any of the
-        symbols encountered are instances of the generic Symbol class, they are
-        specialised (in place) and become instances of DataSymbol with
-        UnresolvedType.
+        containing the lower and upper bound of that dimension.
 
         :param dimensions: fparser dimension attribute.
-        :type dimensions: \
+        :type dimensions:
             :py:class:`fparser.two.Fortran2003.Dimension_Attr_Spec`
         :param symbol_table: symbol table of the declaration context.
         :type symbol_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
 
-        :returns: shape of the attribute in column-major order (leftmost \
-            index is contiguous in memory). Each entry represents an array \
-            dimension. If it is 'None' the extent of that dimension is \
-            unknown, otherwise it holds a 2-tuple with the upper and lower \
-            bounds of the dimension. If it is an empty list then the symbol \
+        :returns: shape of the attribute in column-major order (leftmost
+            index is contiguous in memory). Each entry represents an array
+            dimension. If it is 'None' the extent of that dimension is
+            unknown, otherwise it holds a 2-tuple with the upper and lower
+            bounds of the dimension. If it is an empty list then the symbol
             represents a scalar.
-        :rtype: list of NoneType or 2-tuples of \
-                :py:class:`psyclone.psyir.nodes.DataNode`
+        :rtype: list[Optional[
+            tuple[:py:class:`psyclone.psyir.nodes.DataNode`,
+                  :py:class:`psyclone.psyir.nodes.DataNode`]]]
 
-        :raises NotImplementedError: if anything other than scalar, integer \
-            literals or symbols are encounted in the dimensions list.
+        :raises GenerationError: if invalid Fortran is encounted in the
+                                 dimensions list.
+        :raises NotImplementedError: if the supplied dimension represents an
+                                     assumed-size specification.
+        :raises InternalError: if a dimension is not an Assumed_Shape_Spec,
+                               Explicit_Shape_Spec or Assumed_Size_Spec.
 
         '''
-        def _process_bound(bound_expr):
-            '''Process the supplied fparser2 parse tree for the upper/lower
-            bound of a dimension in an array declaration.
-
-            :param bound_expr: fparser2 parse tree for lower/upper bound.
-            :type bound_expr: :py:class:`fparser.two.utils.Base`
-
-            :returns: PSyIR for the bound.
-            :rtype: :py:class:`psyclone.psyir.nodes.DataNode`
-
-            :raises NotImplementedError: if an unsupported form of array \
-                                         bound is found.
-            :raises GenerationError: invalid Fortran declaration of an \
-                upper bound without an associated lower bound.
-
-            '''
-            if isinstance(bound_expr, Fortran2003.Int_Literal_Constant):
-                return Literal(bound_expr.items[0], INTEGER_TYPE)
-
-            if isinstance(bound_expr, Fortran2003.Name):
-                # Fortran does not regulate the order in which variables
-                # may be declared so it's possible for the shape
-                # specification of an array to reference variables that
-                # come later in the list of declarations. The reference
-                # may also be to a symbol present in a parent symbol table
-                # (e.g. if the variable is declared in an outer, module
-                # scope).
-                dim_name = bound_expr.string.lower()
-                try:
-                    sym = symbol_table.lookup(dim_name)
-                    # pylint: disable=unidiomatic-typecheck
-                    if type(sym) is Symbol:
-                        # An entry for this symbol exists but it's only a
-                        # generic Symbol and we now know it must be a
-                        # DataSymbol.
-                        sym.specialise(DataSymbol, datatype=UnresolvedType())
-                    elif isinstance(sym.datatype, (UnsupportedType,
-                                                   UnresolvedType)):
-                        # Allow symbols of Unsupported/UnresolvedType.
-                        pass
-                    elif not (isinstance(sym.datatype, ScalarType) and
-                              sym.datatype.intrinsic ==
-                              ScalarType.Intrinsic.INTEGER):
-                        # It's not of Unsupported/UnresolvedType and it's not
-                        # an integer scalar.
-                        raise NotImplementedError(
-                                "Unsupported shape dimension")
-                except KeyError:
-                    # We haven't seen this symbol before so create a new
-                    # one with a unresolved interface (since we don't
-                    # currently know where it is declared).
-                    sym = DataSymbol(dim_name, default_integer_type(),
-                                     interface=UnresolvedInterface())
-                    symbol_table.add(sym)
-                return Reference(sym)
-
-            raise NotImplementedError("Unsupported shape dimension")
-
         one = Literal("1", INTEGER_TYPE)
         shape = []
         # Traverse shape specs in Depth-first-search order
@@ -1403,10 +1362,13 @@ class Fparser2Reader():
                 # ":" -> Assumed_Shape_Spec(None, None)
                 # "4:" -> Assumed_Shape_Spec(Int_Literal_Constant('4', None),
                 #                            None)
-                lower = (_process_bound(dim.children[0]) if dim.children[0]
-                         else None)
+                lower = None
+                if dim.children[0]:
+                    lower = self._process_array_bound(dim.children[0],
+                                                      symbol_table)
                 if dim.children[1]:
-                    upper = _process_bound(dim.children[1])
+                    upper = self._process_array_bound(dim.children[1],
+                                                      symbol_table)
                 else:
                     upper = ArrayType.Extent.ATTRIBUTE if lower else None
 
@@ -1421,19 +1383,15 @@ class Fparser2Reader():
                     shape.append(None)
 
             elif isinstance(dim, Fortran2003.Explicit_Shape_Spec):
-                try:
-                    upper = _process_bound(dim.items[1])
-                    if dim.items[0]:
-                        lower = _process_bound(dim.items[0])
-                        shape.append((lower, upper))
-                    else:
-                        # Lower bound defaults to 1 in Fortran
-                        shape.append((one.copy(), upper))
-                except NotImplementedError as err:
-                    raise NotImplementedError(
-                        f"Could not process {dimensions}. Only scalar integer "
-                        f"literals or symbols are supported for explicit-shape"
-                        f" array declarations.") from err
+                upper = self._process_array_bound(dim.items[1],
+                                                  symbol_table)
+                if dim.items[0]:
+                    lower = self._process_array_bound(dim.items[0],
+                                                      symbol_table)
+                    shape.append((lower, upper))
+                else:
+                    # Lower bound defaults to 1 in Fortran
+                    shape.append((one.copy(), upper))
 
             elif isinstance(dim, Fortran2003.Assumed_Size_Spec):
                 raise NotImplementedError(
@@ -2073,9 +2031,16 @@ class Fparser2Reader():
                 # scalar
                 datatype = base_type
 
-            # Make sure the declared symbol exists in the SymbolTable
+            # Make sure the declared symbol exists in the SymbolTable.
             tag = None
             try:
+                if isinstance(decl, Fortran2003.Data_Component_Def_Stmt):
+                    # We are dealing with the declaration of a component of a
+                    # structure. This must be a new entity and therefore we do
+                    # not want to attempt to lookup a symbol with this name -
+                    # trigger the exception path to create a new, local symbol.
+                    raise KeyError
+
                 sym = symbol_table.lookup(sym_name, scope_limit=scope)
                 # pylint: disable=unidiomatic-typecheck
                 if type(sym) is Symbol:
@@ -2143,13 +2108,13 @@ class Fparser2Reader():
         :type parent: :py:class:`psyclone.psyGen.KernelSchedule`
         :param decl: fparser2 parse tree of declaration to process.
         :type decl: :py:class:`fparser.two.Fortran2003.Type_Declaration_Stmt`
-        :param visibility_map: mapping of symbol name to visibility (for \
+        :param visibility_map: mapping of symbol name to visibility (for
             those symbols listed in an accessibility statement).
-        :type visibility_map: dict with str keys and \
-            :py:class:`psyclone.psyir.symbols.Symbol.Visibility` values
+        :type visibility_map: dict[str,
+            :py:class:`psyclone.psyir.symbols.Symbol.Visibility`]
 
-        :raises SymbolError: if a Symbol already exists with the same name \
-            as the derived type being defined and it is not a DataTypeSymbol \
+        :raises SymbolError: if a Symbol already exists with the same name
+            as the derived type being defined and it is not a DataTypeSymbol
             or is not of UnresolvedType.
 
         '''
@@ -2222,15 +2187,28 @@ class Fparser2Reader():
                 raise NotImplementedError(
                     "Derived-type definition has a CONTAINS statement.")
 
-            # Re-use the existing code for processing symbols
-            local_table = SymbolTable(
-                default_visibility=default_compt_visibility)
+            # Re-use the existing code for processing symbols. This needs to
+            # be able to find any symbols declared in an outer scope but
+            # referenced within the type definition (e.g. a type name). Hence
+            # the table needs to be connected to the tree.
+            local_table = Container("tmp", parent=parent).symbol_table
+            local_table.default_visibility = default_compt_visibility
+
             for child in walk(decl, Fortran2003.Data_Component_Def_Stmt):
                 self._process_decln(parent, local_table, child)
-            # Convert from Symbols to type information
+            # Convert from Symbols to StructureType components.
             for symbol in local_table.symbols:
-                dtype.add(symbol.name, symbol.datatype, symbol.visibility,
-                          symbol.initial_value)
+                if symbol.is_unresolved:
+                    # If a Symbol is unresolved then it isn't defined within
+                    # this structure so we add it to the parent SymbolTable.
+                    # (This can happen when e.g. an array member is dimensioned
+                    # by a parameter declared elsewhere.)
+                    parent.symbol_table.add(symbol)
+                else:
+                    datatype = symbol.datatype
+                    initial_value = symbol.initial_value
+                    dtype.add(symbol.name, datatype, symbol.visibility,
+                              initial_value)
 
             # Update its type with the definition we've found
             tsymbol.datatype = dtype
@@ -2290,7 +2268,7 @@ class Fparser2Reader():
                 node.items[1].items = tuple(entry_list)
 
         # Try to parse the modified node.
-        symbol_table = SymbolTable()
+        symbol_table = scope.symbol_table
         try:
             self._process_decln(scope, symbol_table, node,
                                 visibility_map)
@@ -2299,6 +2277,9 @@ class Fparser2Reader():
             new_sym = symbol_table.lookup(symbol_name)
             datatype = new_sym.datatype
             init_expr = new_sym.initial_value
+            # Remove the Symbol that has just been added as it doesn't have
+            # all the necessary properties.
+            symbol_table._symbols.pop(new_sym.name)
         except NotImplementedError:
             datatype = None
             init_expr = None
@@ -4273,14 +4254,7 @@ class Fparser2Reader():
                     if array.is_full_range(idx):
                         # The access to this index is to the full range of
                         # the array.
-                        # TODO #949 - ideally we would try to find the lower
-                        # bound of the array by interrogating `array.symbol.
-                        # datatype` but the fparser2 frontend doesn't currently
-                        # support array declarations with explicit lower bounds
-                        lbound = IntrinsicCall.create(
-                            IntrinsicCall.Intrinsic.LBOUND,
-                            [base_ref.copy(),
-                             ("dim", Literal(str(idx+1), INTEGER_TYPE))])
+                        lbound = array.get_lbound_expression(idx)
                     else:
                         # We need the lower bound of this access.
                         lbound = child.start.copy()
@@ -4462,6 +4436,9 @@ class Fparser2Reader():
         integer_type = default_integer_type()
 
         # Now create a loop nest of depth `rank`
+        add_op = BinaryOperation.Operator.ADD
+        sub_op = BinaryOperation.Operator.SUB
+        one = Literal("1", INTEGER_TYPE)
         new_parent = parent
         for idx in range(rank, 0, -1):
 
@@ -4488,7 +4465,16 @@ class Fparser2Reader():
                                           ("dim", Literal(str(idx),
                                                           integer_type))]))
             else:
-                loop.addchild(mask_shape[idx-1].upper.copy())
+                lbound = mask_shape[idx-1].lower
+                if isinstance(lbound, Literal) and lbound.value == "1":
+                    # Lower bound is unity so size is just the upper bound.
+                    expr2 = mask_shape[idx-1].upper.copy()
+                else:
+                    # Size = upper-bound - lower-bound + 1
+                    expr = BinaryOperation.create(
+                        sub_op, mask_shape[idx-1].upper.copy(), lbound.copy())
+                    expr2 = BinaryOperation.create(add_op, expr, one.copy())
+                loop.addchild(expr2)
 
             # Add loop increment
             loop.addchild(Literal("1", integer_type))
diff --git a/src/psyclone/psyir/nodes/array_mixin.py b/src/psyclone/psyir/nodes/array_mixin.py
index 0e7b0f272..5c992f063 100644
--- a/src/psyclone/psyir/nodes/array_mixin.py
+++ b/src/psyclone/psyir/nodes/array_mixin.py
@@ -433,12 +433,15 @@ class ArrayMixin(metaclass=abc.ABCMeta):
         if isinstance(datatype.shape[index], ArrayType.Extent):
             # The size is unspecified at compile-time (but is
             # available at run-time e.g. when the size is allocated by
-            # an allocate statement.
+            # an allocate statement).
             return False
 
         # The size of the bound is available.
         if bound_type == "upper":
             declaration_bound = datatype.shape[index].upper
+            if isinstance(declaration_bound, ArrayType.Extent):
+                # But only at run-time.
+                return False
         else:
             declaration_bound = datatype.shape[index].lower
 


Test patch:
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py
index f1b4f0394..9780181c5 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py
@@ -61,8 +61,8 @@ from psyclone.psyir.nodes import (
     RegionDirective, Routine, StandaloneDirective, StructureReference,
     ArrayOfStructuresReference, Call, IntrinsicCall)
 from psyclone.psyir.symbols import (
-    DataSymbol, ContainerSymbol, SymbolTable, ArgumentInterface,
-    SymbolError, ScalarType, ArrayType, INTEGER_TYPE, REAL_TYPE, RoutineSymbol,
+    DataSymbol, ContainerSymbol, ArgumentInterface, ArrayType,
+    SymbolError, ScalarType, INTEGER_TYPE, REAL_TYPE, RoutineSymbol,
     UnsupportedFortranType, UnresolvedType, Symbol, UnresolvedInterface,
     ImportInterface, BOOLEAN_TYPE, StaticInterface, UnknownInterface,
     StructureType, DataTypeSymbol)
@@ -1112,62 +1112,25 @@ def test_process_unsupported_declarations(fortran_reader):
     assert ssym.initial_value.symbol.name == "fbsp"
 
 
-@pytest.mark.usefixtures("f2008_parser")
-def test_unsupported_decln_initial_value(monkeypatch):
-    ''' Check that an invalid constant value for a parameter is handled
-    correctly. '''
-    fake_parent = KernelSchedule("dummy_schedule")
-    reader = FortranStringReader(
-        "INTEGER, PRIVATE, PARAMETER :: happy=1, "
-        "fbsp=SELECTED_REAL_KIND(6,37), sad=fbsp")
-    fparser2spec = Specification_Part(reader).content[0]
-    # This error condition is very difficult to trigger so we monkeypatch
-    # the DataSymbol class itself with a setter that raises a ValueError
-    # for anything other than a Literal.
-
-    class BrokenDataSymbol(DataSymbol):
-        ''' Sub-class of DataSymbol with `initial_value` setter patched
-        so that it raises a ValueError for anything other than a Literal. '''
-        @property
-        def initial_value(self):
-            return self._initial_value
-
-        @initial_value.setter
-        def initial_value(self, value):
-            if isinstance(value, Literal):
-                self._initial_value = value
-            else:
-                raise ValueError("")
-
-    # At this point the fparser2 module will already have 'DataSymbol' in
-    # its namespace (due to the imports at the top of this file) so we
-    # monkeypatch that entry.
-    # pylint: disable=import-outside-toplevel
-    from psyclone.psyir.frontend import fparser2
-    monkeypatch.setattr(fparser2, "DataSymbol", BrokenDataSymbol)
+def test_unsupported_decln_function_type(fortran_reader):
+    '''
+    Check that the frontend raises the expected error if it hits trouble
+    while creating a DataSymbol representing the return value of a function.
 
-    processor = Fparser2Reader()
-    processor.process_declarations(fake_parent, [fparser2spec], [])
-    hsym = fake_parent.symbol_table.lookup("happy")
-    assert hsym.datatype.intrinsic == ScalarType.Intrinsic.INTEGER
-    assert hsym.initial_value.value == "1"
-    fbsym = fake_parent.symbol_table.lookup("fbsp")
-    assert isinstance(fbsym.datatype, UnsupportedFortranType)
-    assert (fbsym.datatype.declaration == "INTEGER, PRIVATE, PARAMETER :: "
-            "fbsp = SELECTED_REAL_KIND(6, 37)")
-    sadsym = fake_parent.symbol_table.lookup("sad")
-    assert isinstance(sadsym.datatype, UnsupportedFortranType)
-    assert (sadsym.datatype.declaration == "INTEGER, PRIVATE, PARAMETER :: "
-            "sad = fbsp")
-
-    # Now do the same but the UnsupportedType constant_value is also the symbol
-    # tagged as 'own_routine_symbol'. This is not recoverable.
-    fake_parent = KernelSchedule("fbsp")
+    '''
+    code = '''
+    module my_mod
+    contains
+    function problem()
+      ! Deliberately broken Fortran - a PARAMETER without an initial value.
+      real, parameter :: problem
+    end function problem
+    end module my_mod
+    '''
     with pytest.raises(InternalError) as err:
-        processor.process_declarations(fake_parent, [fparser2spec], [])
-    assert ("The fparser2 frontend does not support declarations where the "
-            "routine name is of UnsupportedType, but found this case in "
-            "'fbsp'." in str(err.value))
+        _ = fortran_reader.psyir_from_source(code)
+    assert ("declarations where the routine name is of UnsupportedType, but "
+            "found this case in 'problem'" in str(err.value))
 
 
 @pytest.mark.usefixtures("f2008_parser")
@@ -1401,6 +1364,40 @@ def test_process_array_declarations():
     assert symbol.datatype.shape[0].upper == ArrayType.Extent.ATTRIBUTE
 
 
+@pytest.mark.usefixtures("f2008_parser")
+def test_process_array_declarations_bound_expressions():
+    ''' Test that Fparser2Reader.process_declarations() handles
+    array declarations that use expressions to specify the bounds.
+    '''
+    fake_parent = KernelSchedule("dummy_schedule")
+    processor = Fparser2Reader()
+
+    # Simple expression for upper bound
+    reader = FortranStringReader("integer :: l3(l1+1)")
+    fparser2spec = Specification_Part(reader).content[0]
+    processor.process_declarations(fake_parent, [fparser2spec], [])
+    l3_var = fake_parent.symbol_table.lookup("l3")
+    dtype = l3_var.datatype
+    assert isinstance(dtype, ArrayType)
+    assert isinstance(dtype.shape[0], ArrayType.ArrayBounds)
+    assert dtype.shape[0].lower.value == "1"
+    assert isinstance(dtype.shape[0].upper, BinaryOperation)
+    # Complicated expressions using intrinsics.
+    reader = FortranStringReader(
+        "integer :: l5(nint(minval(l4)):nint(maxval(l4)))")
+    fparser2spec = Specification_Part(reader).content[0]
+    processor.process_declarations(fake_parent, [fparser2spec], [])
+    l5_var = fake_parent.symbol_table.lookup("l5")
+    l5dtype = l5_var.datatype
+    assert isinstance(l5dtype, ArrayType)
+    assert isinstance(l5dtype.shape[0], ArrayType.ArrayBounds)
+    assert isinstance(l5dtype.shape[0].lower, IntrinsicCall)
+    assert l5dtype.shape[0].lower.intrinsic is IntrinsicCall.Intrinsic.NINT
+    assert isinstance(l5dtype.shape[0].upper, IntrinsicCall)
+    assert l5dtype.shape[0].upper.intrinsic is IntrinsicCall.Intrinsic.NINT
+    assert l5dtype.shape[0].upper.debug_string() == "NINT(MAXVAL(l4))"
+
+
 @pytest.mark.usefixtures("f2008_parser")
 def test_process_not_supported_declarations():
     '''Test that process_declarations method raises the proper errors when
@@ -1666,22 +1663,24 @@ def test_process_declarations_unsupported_node():
 def test_parse_array_dimensions_attributes():
     '''Test that process_declarations method parses multiple specifications
     of array attributes.
-    '''
 
-    sym_table = SymbolTable()
+    '''
+    processor = Fparser2Reader()
+    sched = KernelSchedule("a_test")
+    sym_table = sched.symbol_table
     reader = FortranStringReader("dimension(:)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert shape == [None]
 
     reader = FortranStringReader("dimension(:,:,:)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert shape == [None, None, None]
 
     reader = FortranStringReader("dimension(3,5)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert len(shape) == 2
     assert shape[0][0].value == "1"
     assert shape[0][1].value == "3"
@@ -1693,14 +1692,14 @@ def test_parse_array_dimensions_attributes():
 
     reader = FortranStringReader("dimension(var1)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert len(shape) == 1
     assert shape[0][0].value == "1"
     assert shape[0][1].symbol == sym_table.lookup('var1')
 
     reader = FortranStringReader("dimension(0:3,var1)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     # First dim is specified with both lower and upper bounds so should
     # have a tuple
     assert isinstance(shape[0], tuple)
@@ -1712,7 +1711,7 @@ def test_parse_array_dimensions_attributes():
 
     reader = FortranStringReader("dimension(0:3,var1:var1_upper)")
     fparser2spec = Dimension_Attr_Spec(reader)
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert isinstance(shape[0], tuple)
     assert len(shape[0]) == 2
     assert shape[0][0].value == "0"
@@ -1726,31 +1725,10 @@ def test_parse_array_dimensions_attributes():
     reader = FortranStringReader("dimension(*)")
     fparser2spec = Dimension_Attr_Spec(reader)
     with pytest.raises(NotImplementedError) as error:
-        _ = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+        _ = processor._parse_dimensions(fparser2spec, sym_table)
     assert "Could not process " in str(error.value)
     assert "Assumed-size arrays are not supported." in str(error.value)
 
-    # Explicit shape symbols must be integer
-    reader = FortranStringReader("dimension(var2)")
-    fparser2spec = Dimension_Attr_Spec(reader)
-    with pytest.raises(NotImplementedError) as error:
-        sym_table.add(DataSymbol("var2", REAL_TYPE))
-        _ = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
-    assert "Could not process " in str(error.value)
-    assert ("Only scalar integer literals or symbols are supported for "
-            "explicit-shape array declarations.") in str(error.value)
-
-    # Explicit shape symbols can only be Literal or Symbol
-    with pytest.raises(NotImplementedError) as error:
-        class UnrecognizedType():
-            '''Type guaranteed to not be part of the _parse_dimensions
-            conditional type handler.'''
-        fparser2spec.items[1].items[0].items[1].__class__ = UnrecognizedType
-        _ = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
-    assert "Could not process " in str(error.value)
-    assert ("Only scalar integer literals or symbols are supported for "
-            "explicit-shape array declarations.") in str(error.value)
-
     # Shape specified by an unknown Symbol
     reader = FortranStringReader("dimension(var3)")
     fparser2spec = Dimension_Attr_Spec(reader)
@@ -1758,19 +1736,17 @@ def test_parse_array_dimensions_attributes():
     vsym = sym_table.new_symbol("var3", interface=ImportInterface(csym))
     # pylint: disable=unidiomatic-typecheck
     assert type(vsym) is Symbol
-    shape = Fparser2Reader._parse_dimensions(fparser2spec, sym_table)
+    shape = processor._parse_dimensions(fparser2spec, sym_table)
     assert len(shape) == 1
     assert shape[0][0].value == "1"
     assert isinstance(shape[0][1], Reference)
-    # Symbol is the same object but is now a DataSymbol
+    # Symbol is the same object.
     assert shape[0][1].symbol is vsym
-    assert isinstance(shape[0][1].symbol, DataSymbol)
     assert shape[0][1].symbol.name == "var3"
     assert isinstance(shape[0][1].symbol.interface, ImportInterface)
 
     # Test dimension and intent arguments together
     fake_parent = KernelSchedule("dummy_schedule")
-    processor = Fparser2Reader()
     reader = FortranStringReader("real, intent(in), dimension(:) :: array3")
     fparser2spec = Specification_Part(reader).content[0]
     processor.process_declarations(fake_parent, [fparser2spec],
@@ -1809,7 +1785,6 @@ def test_unresolved_array_size():
     processor.process_declarations(fake_parent, fparser2spec, [])
     dim_sym = fake_parent.symbol_table.lookup("n")
     assert isinstance(dim_sym.interface, UnresolvedInterface)
-    assert dim_sym.datatype.intrinsic == ScalarType.Intrinsic.INTEGER
     # Check that the lookup of the dimensioning symbol is not case sensitive
     reader = FortranStringReader("real, dimension(N) :: array4")
     fparser2spec = Specification_Part(reader).content
@@ -2003,9 +1978,9 @@ def test_parse_array_dimensions_unhandled(monkeypatch):
     reader = FortranStringReader("dimension(:)")
     fparser2spec = Dimension_Attr_Spec(reader)
     with pytest.raises(InternalError) as error:
-        _ = Fparser2Reader._parse_dimensions(fparser2spec, None)
-    assert "Reached end of loop body and array-shape specification" \
-        in str(error.value)
+        _ = Fparser2Reader()._parse_dimensions(fparser2spec, None)
+    assert ("Reached end of loop body and array-shape specification"
+            in str(error.value))
     assert " has not been handled." in str(error.value)
 
 
@@ -2972,7 +2947,7 @@ def test_structures(fortran_reader, fortran_writer):
     procedures.
 
     '''
-    # derived-type with initial value (StructureType)
+    # derived-type with initial value (StructureType) and in-line visibility
     test_code = (
         "module test_mod\n"
         "    type, private :: my_type\n"
@@ -2992,6 +2967,27 @@ def test_structures(fortran_reader, fortran_writer):
         "    integer, public :: j\n"
         "  end type my_type\n" in result)
 
+    # Repeat but have visibility of symbol specified separately.
+    test_code = (
+        "module test_mod\n"
+        "    private :: my_type\n"
+        "    type :: my_type\n"
+        "      integer :: i = 1\n"
+        "      integer :: j\n"
+        "    end type my_type\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(test_code)
+    sym_table = psyir.children[0].symbol_table
+    symbol = sym_table.lookup("my_type")
+    assert isinstance(symbol, DataTypeSymbol)
+    assert isinstance(symbol.datatype, StructureType)
+    result = fortran_writer(psyir)
+    assert (
+        "  type, private :: my_type\n"
+        "    integer, public :: i = 1\n"
+        "    integer, public :: j\n"
+        "  end type my_type\n" in result)
+
     # type that extends another type (UnsupportedFortranType)
     test_code = (
         "module test_mod\n"
@@ -3191,3 +3187,39 @@ def test_structures_constant_use(fortran_reader, fortran_writer):
         "      integer :: i = N + M\n"
         "      integer :: j\n"
         "    end type my_type\n" in result)
+
+
+def test_structures_duplicate_name(f2008_parser):
+    '''
+    Check that the datatype of a structure member correctly refers to
+    a DataTypeSymbol in the parent scope.
+    '''
+    test_code = '''\
+    subroutine test()
+      integer, parameter :: nelem = 10
+      type :: y
+        integer, dimension(3) :: jp
+      end type
+      type :: x
+        type(y), dimension(nelem) :: y
+      end type
+    end subroutine'''
+    reader = FortranStringReader(test_code)
+    ptree = f2008_parser(reader)
+    processor = Fparser2Reader()
+    psyir = processor.generate_psyir(ptree)
+    routine = psyir.walk(Routine)[0]
+    table = routine.symbol_table
+    xsym = table.lookup("x")
+    ysym = table.lookup("y")
+    nelem = table.lookup("nelem")
+    assert isinstance(xsym, DataTypeSymbol)
+    dtype = xsym.datatype
+    assert isinstance(dtype, StructureType)
+    ycompt = dtype.components["y"]
+    # The datatype of the member 'y' must be the 'y' DataTypeSymbol.
+    assert isinstance(ycompt.datatype, ArrayType)
+    assert ycompt.datatype.intrinsic is ysym
+    # Its shape must refer to "nelem" in the table of the Routine.
+    assert isinstance(ycompt.datatype.shape[0].upper, Reference)
+    assert ycompt.datatype.shape[0].upper.symbol is nelem
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py b/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py
index 462572979..a6ff7ca51 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_where_handler_test.py
@@ -424,9 +424,6 @@ def test_where_mask_starting_value(fortran_reader, fortran_writer):
     Check handling of a case where the mask array is indexed from values other
     than unity.
 
-    # TODO #949 - we can't currently take advantage of any knowledge of the
-    # declared lower bounds of arrays because the fparser2 frontend doesn't yet
-    # capture this information (we get an UnsupportedFortranType).
     '''
     code = '''\
     program my_sub
@@ -445,23 +442,17 @@ def test_where_mask_starting_value(fortran_reader, fortran_writer):
 '''
     psyir = fortran_reader.psyir_from_source(code)
     output = fortran_writer(psyir)
-    print(output)
     expected = '''\
-  do widx2 = 1, SIZE(picefr, dim=2), 1
-    do widx1 = 1, SIZE(picefr, dim=1), 1
-      if (picefr(LBOUND(picefr, dim=1) + widx1 - 1,\
-LBOUND(picefr, dim=2) + widx2 - 1) > 1.e-10) then
-        zevap_ice(LBOUND(zevap_ice, dim=1) + widx1 - 1,\
-LBOUND(zevap_ice, dim=2) + widx2 - 1,1) = \
-snow(LBOUND(snow, dim=1) + widx1 - 1,3,LBOUND(snow, dim=3) + widx2 - 1) * \
+  do widx2 = 1, 5 - (-5) + 1, 1
+    do widx1 = 1, 5 - (-5) + 1, 1
+      if (picefr(-5 + widx1 - 1,-5 + widx2 - 1) > 1.e-10) then
+        zevap_ice(widx1,widx2,1) = snow(-2 + widx1 - 1,3,-3 + widx2 - 1) * \
 frcv(jpr_ievp)%z3(LBOUND(frcv(jpr_ievp)%z3, dim=1) + widx1 - 1,\
 LBOUND(frcv(jpr_ievp)%z3, dim=2) + widx2 - 1,1) / \
-picefr(LBOUND(picefr, dim=1) + widx1 - 1,LBOUND(picefr, dim=2) + widx2 - 1)
+picefr(-5 + widx1 - 1,-5 + widx2 - 1)
       else
-        zevap_ice(LBOUND(zevap_ice, dim=1) + widx1 - 1,\
-LBOUND(zevap_ice, dim=2) + widx2 - 1,1) = \
-snow(LBOUND(snow, dim=1) + widx1 - 1,map(jpl),\
-LBOUND(snow, dim=3) + widx2 - 1) + slush(-22 + widx1 - 1,jpl,-32 + widx2 - 1)
+        zevap_ice(widx1,widx2,1) = snow(-2 + widx1 - 1,map(jpl),\
+-3 + widx2 - 1) + slush(-22 + widx1 - 1,jpl,-32 + widx2 - 1)
 '''
     assert expected in output
 
@@ -496,6 +487,37 @@ def test_where_mask_is_slice(fortran_reader, fortran_writer):
     assert "if (picefr(widx1,jstart + widx2 - 1) > 4.e-10)" in out
 
 
+def test_where_mask_is_slice_lower_limit(fortran_reader, fortran_writer):
+    '''
+    Check that the correct loop bounds and index expressions are created
+    when the mask expression uses a slice and the array itself is a formal
+    argument with a specified lower bound and no explicit upper bound.
+    '''
+    code = '''\
+    subroutine my_sub(picefr)
+      use some_mod
+      real, dimension(3:,2:) :: picefr
+      WHERE( picefr(:4,jstart:) > 1.e-10 )
+        zevap_ice(:,:,1) = frcv(jpr_ievp)%z3(:,:,1) / picefr(:,:)
+      ELSEWHERE ( picefr(4:5,jstart:) > 4.e-10)
+        zevap_ice(:,:,1) = 0.0
+      END WHERE
+    end subroutine my_sub
+'''
+    psyir = fortran_reader.psyir_from_source(code)
+    out = fortran_writer(psyir)
+    # Check that created loops have the correct number of iterations
+    assert "do widx2 = 1, UBOUND(picefr, dim=2) - jstart + 1, 1" in out
+    assert "do widx1 = 1, 4 - LBOUND(picefr, dim=1) + 1, 1" in out
+    # Check that the indexing into the mask expression uses the lower bounds
+    # specified in the original slice.
+    assert ("if (picefr(LBOUND(picefr, dim=1) + widx1 - 1,"
+            "jstart + widx2 - 1) > 1.e-10)" in out)
+    assert ("zevap_ice(LBOUND(zevap_ice, dim=1) + widx1 - 1,"
+            "LBOUND(zevap_ice, dim=2) + widx2 - 1,1) = 0.0" in out)
+    assert "if (picefr(4 + widx1 - 1,jstart + widx2 - 1) > 4.e-10)" in out
+
+
 def test_where_body_containing_sum_with_dim(fortran_reader, fortran_writer):
     '''
     Since a SUM(x, dim=y) performs a reduction but produces an array, we need
diff --git a/src/psyclone/tests/psyir/nodes/array_mixin_test.py b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
index ce3b2f2ff..f9290e82b 100644
--- a/src/psyclone/tests/psyir/nodes/array_mixin_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
@@ -153,17 +153,34 @@ def test_is_upper_lower_bound(fortran_reader):
     class.
 
     '''
-    code = (
-        "subroutine test()\n"
-        "real a(n)\n"
-        "a(1:n) = 0.0\n"
-        "end subroutine\n")
-
-    # Return True as the symbolic values of the declaration and array
-    # reference match.
+    code = '''
+    subroutine test(kttrd, ptrd)
+      use some_mod
+      real a(n-1)
+      integer, dimension(2), intent(in) :: kttrd
+      real, dimension(kttrd(1):,kttrd(2):,:), intent(in) ::   ptrd
+      trdt(ntsi-(0):,ntsj-(0):ntej+(0),:) =    &
+         ptrd(ntsi-(0):ntei+(0),ntsj-(0):ntej+(0),:)
+      a(1:n-1) = 0.0
+    end subroutine
+    '''
     psyir = fortran_reader.psyir_from_source(code)
     assigns = psyir.walk(Assignment)
-    array_ref = assigns[0].lhs
+    trdt_ref = assigns[0].lhs
+    assert not trdt_ref.is_lower_bound(0)
+    assert trdt_ref.is_upper_bound(0)
+    assert not trdt_ref.is_upper_bound(1)
+    assert not trdt_ref.is_lower_bound(1)
+    assert trdt_ref.is_upper_bound(2)
+    assert trdt_ref.is_lower_bound(2)
+    ptrd_ref = assigns[0].rhs
+    assert not ptrd_ref.is_lower_bound(0)
+    assert not ptrd_ref.is_upper_bound(0)
+    assert ptrd_ref.is_lower_bound(2)
+    assert ptrd_ref.is_upper_bound(2)
+    # Return True as the symbolic values of the declaration and array
+    # reference match.
+    array_ref = assigns[1].lhs
     assert array_ref.is_lower_bound(0)
     assert array_ref.is_upper_bound(0)
 
@@ -302,13 +319,11 @@ def test_is_bound_extent(fortran_reader):
     ("10", "1", True, False), ("10", "10", False, True),
     ("10", "5", False, False), ("n", "1", True, False),
     ("n", "n", False, True), ("n", "n-4", False, False),
-    ("10", "5+5", False, True)])
+    ("10", "5+5", False, True), ("n+8", "n+8", False, True),
+    ("n-5:n+5", "n-5", True, False)])
 def test_is_bound_access(fortran_reader, bounds, access, lower, upper):
     '''Test the _is_bound method returns True when the array access
-    matches the array declaration and False if not. Note, the method
-    supports array declarations that are expressions, however,
-    currently the PSyIR does not recognise these so they can't be
-    tested (TODO issue #949).
+    matches the array declaration and False if not.
 
     '''
     code = (
@@ -341,6 +356,8 @@ def test_is_same_array(fortran_reader):
     """)
 
     assignments = psyir.walk(Assignment)
+    # Argument must be a Member or Reference
+    assert not assignments[0].lhs.is_same_array(Literal("1", INTEGER_TYPE))
     # Check that the array itself is the same, not the accessed index
     assert assignments[0].lhs.is_same_array(assignments[1].lhs)
     # Also works when comparing with a plain reference of the array
@@ -823,28 +840,25 @@ def test_same_range(fortran_reader):
 
     # If the values are implicit, but we know the declaration we can also
     # compare them.
-    # TODO #949: Currently expressions inside shape (e.g. dimension(5-1)),
-    # produce an UnsupportedFortranType but when this is resolved, shape
-    # comparisons should also work symbolically
     code = '''
     subroutine test()
-        real, dimension(1:4, 1:4, 2:5) :: A, C
+        real, dimension(1+0:4, 1:4, 2:4+1) :: A, C
         real, dimension(4,   4,   4) :: B
         A(:,:,:) = B(:,:,:)
         C(:,:4,:4) = 0
     end subroutine
     '''
     psyir = fortran_reader.psyir_from_source(code)
-    array1, array2 = psyir.walk(Assignment)[0].children
-    array3, _ = psyir.walk(Assignment)[1].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 False
+    aref, bref = psyir.walk(Assignment)[0].children
+    cref, _ = psyir.walk(Assignment)[1].children
+    assert aref.same_range(0, bref, 0) is True
+    assert aref.same_range(1, bref, 1) is True
+    assert aref.same_range(2, bref, 2) is False
     # If the type in known, ranges in different assignments can also be
     # compared
-    assert array1.same_range(0, array3, 0) is True
-    assert array1.same_range(1, array3, 1) is True
-    assert array1.same_range(2, array3, 2) is False
+    assert aref.same_range(0, cref, 0) is True
+    assert aref.same_range(1, cref, 1) is True
+    assert aref.same_range(2, cref, 2) is False
 
     # If the values are implicit, and the declaration uses ATTRIBUTE or
     # DEFERRED shape, we return the appropriate results
@@ -868,6 +882,28 @@ def test_same_range(fortran_reader):
     assert array3.same_range(0, array4, 0) is False
     assert array5.same_range(0, array6, 0) is False
 
+    # A mixture of expressions and assumed-size dimensions.
+    code = '''
+    module test_mod
+      use some_mod
+      real, allocatable, dimension(:,:,:) ::   trdt
+    contains
+    subroutine test(kttrd, ptrd)
+      integer, dimension(2), intent(in) :: kttrd
+      real, dimension(kttrd(1):,kttrd(2):,:), intent(in) ::   ptrd
+      trdt(ntsi-(0):ntei+(0),ntsj-(0):ntej+(0),:) =    &
+         ptrd(ntsi-(0):ntei+(0),ntsj-(0):ntej+(0),:) * &
+         tmask(ntsi-(0):ntei+(0),ntsj-(0):ntej+(0),:)
+    end subroutine
+    end module
+    '''
+    psyir = fortran_reader.psyir_from_source(code)
+    assign = psyir.walk(Assignment)[0]
+    lhs = assign.lhs
+    ptrd = assign.rhs.children[0]
+    assert lhs.same_range(0, ptrd, 0) is True
+    assert lhs.same_range(2, ptrd, 2) is False
+
     # This functionality also works with SoA and SoAoS
     code = '''
     subroutine test()