Repository: stfc/PSyclone (python)

Issue:
Add support for querying the data type of a PSyIR expression.
From #1696:

You maybe already hinted it here:

    It may be that we really want the routine (rank_of_subsection) to give us back the shape of any subsection

But this recursion method to find the slice rank is somewhat similar to what @rupertford was planning to implement in order to 'bubble up' the datatypes too all PSyIR expression (datanodes) nodes. I can't find the specific issue but we had a chat during Easter week and Rupert was about to started implementing it. (starting with operations but accessors must also be part of it for a complete solution)

Given that this .datatype property for all expression nodes will be more generic (and would avoid having the slightly confusing "rank_of_subsection" in nodes that have nothing to do with arrays like member and structurereference) I think the methods you add in this PR could at some point be superseded by .datatype and simply be:

rank_slice = len(top_node.datatype.shape)

Note that for a reference `a(3,:)`

    ref.symbol.datatype is ArrayType([3, Extent.ATTRIBUTE])

but

    ref.datatype is ArrayType([Extent.ATTRIBUTE])

which is the information you need.

Pull request description:
(Towards #1799) add support for querying datatype of PSyIR references.
This PR deals with References (and sub-classes) only. It does not tackle expressions.

Patch:
diff --git a/changelog b/changelog
index f8196e1c4..81c3204f8 100644
--- a/changelog
+++ b/changelog
@@ -130,6 +130,9 @@
 	47) PR #1890 for #1889. Fix test to avoid temporary files being
 	left after pytest run.
 
+	48) PR #1850 towards #1799. Initial PSyIR node.datatype implementation
+	to query the resulting datatype of a PSyIR subtree.
+
 release 2.3.1 17th of June 2022
 
 	1) PR #1747 for #1720. Adds support for If blocks to PSyAD.
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index 3115cff9e..aa892bd3a 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -3523,7 +3523,7 @@ class Fparser2Reader(object):
 
         '''
         symbol = _find_or_create_imported_symbol(parent, node.string)
-        return Reference(symbol, parent)
+        return Reference(symbol, parent=parent)
 
     def _parenthesis_handler(self, node, parent):
         '''
@@ -3567,7 +3567,7 @@ class Fparser2Reader(object):
         # part-references instead of function-references.
         symbol = _find_or_create_imported_symbol(parent, reference_name)
 
-        array = ArrayReference(symbol, parent)
+        array = ArrayReference(symbol, parent=parent)
         self.process_nodes(parent=array, nodes=node.items[1].items)
         return array
 
diff --git a/src/psyclone/psyir/nodes/array_mixin.py b/src/psyclone/psyir/nodes/array_mixin.py
index 2022e5414..69e421fba 100644
--- a/src/psyclone/psyir/nodes/array_mixin.py
+++ b/src/psyclone/psyir/nodes/array_mixin.py
@@ -39,21 +39,22 @@
 ''' This module contains the implementation of the abstract ArrayMixin. '''
 
 import abc
-import six
 
 from psyclone.errors import InternalError
+from psyclone.psyir.nodes.call import Call
+from psyclone.psyir.nodes.codeblock import CodeBlock
 from psyclone.psyir.nodes.datanode import DataNode
 from psyclone.psyir.nodes.literal import Literal
 from psyclone.psyir.nodes.member import Member
-from psyclone.psyir.nodes.operation import BinaryOperation
+from psyclone.psyir.nodes.operation import Operation, BinaryOperation
 from psyclone.psyir.nodes.ranges import Range
 from psyclone.psyir.nodes.reference import Reference
 from psyclone.psyir.symbols import SymbolError
-from psyclone.psyir.symbols.datatypes import ScalarType, ArrayType
+from psyclone.psyir.symbols.datatypes import (ScalarType, ArrayType,
+                                              INTEGER_TYPE)
 
 
-@six.add_metaclass(abc.ABCMeta)
-class ArrayMixin(object):
+class ArrayMixin(metaclass=abc.ABCMeta):
     '''
     Abstract class used to add functionality common to Nodes that represent
     Array accesses.
@@ -355,6 +356,76 @@ class ArrayMixin(object):
                     f"expression but found '{type(child).__name__}'")
         return self.children
 
+    def _get_effective_shape(self):
+        '''
+        :returns: the shape of the array access represented by this node.
+        :rtype: List[:py:class:`psyclone.psyir.nodes.DataNode`]
+
+        :raises NotImplementedError: if any of the array-indices involve a
+                                     function call or an expression.
+        '''
+        def _num_elements(expr):
+            '''
+            Create PSyIR for the number of elements in this range. It
+            is given by (stop - start)/step + 1.
+
+            :param expr: the range for which to compute the number of elements.
+            :type expr: :py:class:`psyclone.psyir.nodes.Range` or \
+                :py:class:`psyclone.psyir.symbols.ArrayType.ArrayBounds`
+
+            :returns: the PSyIR expression for the number of elements in the \
+                      supplied range.
+            :rtype: :py:class:`psyclone.psyir.nodes.BinaryOperation`
+
+            '''
+            if isinstance(expr, Range):
+                start = expr.start
+                stop = expr.stop
+                step = expr.step
+            elif isinstance(expr, ArrayType.ArrayBounds):
+                start = expr.lower
+                stop = expr.upper
+                step = Literal("1", INTEGER_TYPE)
+            minus = BinaryOperation.create(BinaryOperation.Operator.SUB,
+                                           stop.copy(), start.copy())
+            div = BinaryOperation.create(BinaryOperation.Operator.DIV,
+                                         minus, step.copy())
+            plus = BinaryOperation.create(BinaryOperation.Operator.ADD,
+                                          div, Literal("1", INTEGER_TYPE))
+            return plus
+
+        shape = []
+        for idx_expr in self.indices:
+            if isinstance(idx_expr, Range):
+                shape.append(_num_elements(idx_expr))
+
+            elif isinstance(idx_expr, Reference):
+                dtype = idx_expr.datatype
+                if dtype.shape:
+                    # An array slice can be defined by a 1D slice of another
+                    # array, e.g. `a(b(1:4))`.
+                    if len(dtype.shape) > 1:
+                        raise InternalError(
+                            f"An array defining a slice of a dimension of "
+                            f"another array must be 1D but '{idx_expr.name}' "
+                            f"used to index into '{self.name}' has "
+                            f"{len(dtype.shape)} dimensions.")
+                    shape.append(_num_elements(dtype.shape[0]))
+            elif isinstance(idx_expr, (Call, Operation, CodeBlock)):
+                # We can't yet straightforwardly query the type of a function
+                # call or Operation - TODO #1799.
+                # pylint: disable=import-outside-toplevel
+                from psyclone.psyir.backend.fortran import FortranWriter
+                # TODO #1887 - get type of writer to use from Config object?
+                fvisitor = FortranWriter()
+                raise NotImplementedError(
+                    f"The array index expressions for access "
+                    f"'{fvisitor(self)}' include a function call or "
+                    f"expression. Querying the return type of "
+                    f"such things is yet to be implemented.")
+
+        return shape
+
     def get_outer_range_index(self):
         ''' Return the index of the child that represents the outermost
         array dimension with a Range construct.
diff --git a/src/psyclone/psyir/nodes/array_of_structures_reference.py b/src/psyclone/psyir/nodes/array_of_structures_reference.py
index f2512e69f..497a1bccd 100644
--- a/src/psyclone/psyir/nodes/array_of_structures_reference.py
+++ b/src/psyclone/psyir/nodes/array_of_structures_reference.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2021, Science and Technology Facilities Council.
+# Copyright (c) 2020-2022, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,13 +37,11 @@
 ''' This module contains the implementation of the ArrayOfStructuresReference
 node. '''
 
-from __future__ import absolute_import
-
 # Circular import if only '...nodes' is used:
-from psyclone.psyir.nodes.structure_reference import StructureReference
 from psyclone.psyir import symbols
-from psyclone.psyir.nodes.array_of_structures_mixin import \
-    ArrayOfStructuresMixin
+from psyclone.psyir.nodes.array_of_structures_mixin import (
+    ArrayOfStructuresMixin)
+from psyclone.psyir.nodes.structure_reference import StructureReference
 
 
 class ArrayOfStructuresReference(ArrayOfStructuresMixin, StructureReference):
diff --git a/src/psyclone/psyir/nodes/array_reference.py b/src/psyclone/psyir/nodes/array_reference.py
index 134538c2a..1c77156f0 100644
--- a/src/psyclone/psyir/nodes/array_reference.py
+++ b/src/psyclone/psyir/nodes/array_reference.py
@@ -38,10 +38,10 @@
 
 ''' This module contains the implementation of the ArrayReference node. '''
 
-from __future__ import absolute_import
 from psyclone.psyir.nodes.array_mixin import ArrayMixin
 from psyclone.psyir.nodes.reference import Reference
-from psyclone.psyir.symbols import DataSymbol, DeferredType, UnknownType
+from psyclone.psyir.symbols import (DataSymbol, DeferredType, UnknownType,
+                                    ScalarType, ArrayType)
 from psyclone.errors import GenerationError
 
 
@@ -100,11 +100,26 @@ class ArrayReference(ArrayMixin, Reference):
         return array
 
     def __str__(self):
-        result = super(ArrayReference, self).__str__() + "\n"
+        result = super().__str__() + "\n"
         for entity in self._children:
             result += str(entity) + "\n"
         return result
 
+    @property
+    def datatype(self):
+        '''
+        :returns: the datatype of the accessed array element(s).
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+        '''
+        shape = self._get_effective_shape()
+        if shape:
+            return ArrayType(self.symbol.datatype, shape)
+        # TODO #1857: Really we should just be able to return
+        # self.symbol.datatype here but currently arrays of scalars are
+        # handled in a different way to all other types of array.
+        return ScalarType(self.symbol.datatype.intrinsic,
+                          self.symbol.datatype.precision)
+
 
 # For AutoAPI documentation generation
 __all__ = ['ArrayReference']
diff --git a/src/psyclone/psyir/nodes/reference.py b/src/psyclone/psyir/nodes/reference.py
index 60c7f19a1..f7d1e7a35 100644
--- a/src/psyclone/psyir/nodes/reference.py
+++ b/src/psyclone/psyir/nodes/reference.py
@@ -39,10 +39,9 @@
 
 ''' This module contains the implementation of the Reference node.'''
 
-from __future__ import absolute_import
+from psyclone.core import AccessType, Signature
 from psyclone.psyir.nodes.datanode import DataNode
 from psyclone.psyir.nodes.operation import Operation, BinaryOperation
-from psyclone.core import AccessType, Signature
 from psyclone.psyir.symbols import Symbol
 
 
@@ -52,8 +51,8 @@ class Reference(DataNode):
 
     :param symbol: the symbol being referenced.
     :type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
-    :param parent: the parent node of this Reference in the PSyIR.
-    :type parent: :py:class:`psyclone.psyir.nodes.Node` or NoneType
+    :param kwargs: additional keyword arguments provided to the super class.
+    :type kwargs: unwrapped dict.
 
     '''
     # Textual description of the node.
@@ -61,8 +60,8 @@ class Reference(DataNode):
     _text_name = "Reference"
     _colour = "yellow"
 
-    def __init__(self, symbol, parent=None):
-        super(Reference, self).__init__(parent=parent)
+    def __init__(self, symbol, **kwargs):
+        super().__init__(**kwargs)
         self.symbol = symbol
 
     def __eq__(self, other):
@@ -174,6 +173,14 @@ class Reference(DataNode):
                 index.reference_accesses(var_accesses)
         var_accesses.add_access(sig, AccessType.READ, self, all_indices)
 
+    @property
+    def datatype(self):
+        '''
+        :returns: the datatype of this reference.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+        '''
+        return self.symbol.datatype
+
 
 # For AutoAPI documentation generation
 __all__ = ['Reference']
diff --git a/src/psyclone/psyir/nodes/structure_reference.py b/src/psyclone/psyir/nodes/structure_reference.py
index b3ec2b345..beaebbc23 100644
--- a/src/psyclone/psyir/nodes/structure_reference.py
+++ b/src/psyclone/psyir/nodes/structure_reference.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2021, Science and Technology Facilities Council.
+# Copyright (c) 2020-2022, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,18 +37,17 @@
 
 ''' This module contains the implementation of the StructureReference node. '''
 
-from __future__ import absolute_import
-import six
-
 from psyclone.core import Signature
 from psyclone.psyir.nodes.reference import Reference
 from psyclone.psyir.nodes.member import Member
 from psyclone.psyir.nodes.array_member import ArrayMember
+from psyclone.psyir.nodes.array_mixin import ArrayMixin
 from psyclone.psyir.nodes.array_of_structures_member import \
     ArrayOfStructuresMember
 from psyclone.psyir.nodes.structure_member import StructureMember
-from psyclone.psyir.symbols import DataSymbol, DataTypeSymbol, StructureType, \
-    DeferredType, UnknownType
+from psyclone.psyir.symbols import (DataSymbol, DataTypeSymbol, StructureType,
+                                    ArrayType, DeferredType, ScalarType,
+                                    UnknownType)
 from psyclone.errors import InternalError
 
 
@@ -168,7 +167,7 @@ class StructureReference(Reference):
         if isinstance(members[-1], tuple):
             # An access to one or more array elements
             subref = ArrayMember.create(members[-1][0], members[-1][1])
-        elif isinstance(members[-1], six.string_types):
+        elif isinstance(members[-1], str):
             # A member access
             subref = Member(members[-1])
         else:
@@ -188,7 +187,7 @@ class StructureReference(Reference):
                 # This is an array access so we have an ArrayOfStructuresMember
                 subref = ArrayOfStructuresMember.create(
                     component[0], component[1], subref)
-            elif isinstance(component, six.string_types):
+            elif isinstance(component, str):
                 # No array access so just a StructureMember
                 subref = StructureMember.create(component, subref)
             else:
@@ -203,7 +202,7 @@ class StructureReference(Reference):
         return ref
 
     def __str__(self):
-        result = super(StructureReference, self).__str__()
+        result = super().__str__()
         for entity in self._children:
             result += "\n" + str(entity)
         return result
@@ -233,13 +232,112 @@ class StructureReference(Reference):
 
         '''
         # Get the signature of self:
-        my_sig, my_index = \
-            super(StructureReference, self).get_signature_and_indices()
+        my_sig, my_index = super().get_signature_and_indices()
         # Then the sub-signature of the member, and indices used:
         sub_sig, indices = self.children[0].get_signature_and_indices()
         # Combine signature and indices
         return (Signature(my_sig, sub_sig), my_index + indices)
 
+    @property
+    def datatype(self):
+        '''
+        Walks down the list of members making up this reference to determine
+        the type that it refers to.
+
+        In order to minimise code duplication, this method also supports
+        ArrayOfStructuresReference by simply allowing for the case where
+        the starting reference is to an Array.
+
+        :returns: the datatype of this reference.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+
+        :raises NotImplementedError: if the structure reference represents \
+                                     an array of arrays.
+        '''
+        dtype = self.symbol.datatype
+
+        if isinstance(dtype, ArrayType):
+            dtype = dtype.intrinsic
+
+        if isinstance(dtype, DataTypeSymbol):
+            dtype = dtype.datatype
+
+        if isinstance(dtype, (DeferredType, UnknownType)):
+            # We don't know the type of the symbol that defines the type
+            # of this structure.
+            return DeferredType()
+
+        # We do have the definition of this structure - walk down it.
+        cursor = self
+        cursor_type = dtype
+
+        # The next four lines are required when this method is called for an
+        # ArrayOfStructuresReference.
+        if isinstance(cursor, ArrayMixin):
+            # pylint: disable=protected-access
+            shape = cursor._get_effective_shape()
+        else:
+            shape = []
+
+        # Walk down the structure, collecting information on any array slices
+        # as we go.
+        while hasattr(cursor, "member"):
+            cursor = cursor.member
+            cursor_type = cursor_type.components[cursor.name].datatype
+            if isinstance(cursor_type, (UnknownType, DeferredType)):
+                return DeferredType()
+            if isinstance(cursor, ArrayMixin):
+                # pylint: disable=protected-access
+                shape.extend(cursor._get_effective_shape())
+
+        # We've reached the ultimate member of the structure access.
+        if shape:
+            if isinstance(cursor_type, ArrayType):
+                # It's of array type but does it represent a single element,
+                # a slice or a whole array? (We use `children` rather than
+                # `indices` so as to avoid having to check that `cursor` is
+                # an `ArrayMember`.)
+                if cursor.children:
+                    # It has indices so could be a single element or a slice.
+                    # pylint: disable=protected-access
+                    cursor_shape = cursor._get_effective_shape()
+                else:
+                    # No indices so it is an access to a whole array.
+                    cursor_shape = cursor_type.shape
+                if cursor_shape and shape != cursor_shape:
+                    # This ultimate access is an array but we've already
+                    # encountered one or more slices earlier in the access
+                    # expression.
+                    # TODO #1887. Allow the writer to be used in error messages
+                    # to be set in the Config object?
+                    # pylint: disable=import-outside-toplevel
+                    from psyclone.psyir.backend.fortran import FortranWriter
+                    fwriter = FortranWriter()
+                    raise NotImplementedError(
+                        f"Array of arrays not supported: the ultimate member "
+                        f"'{cursor.name}' of the StructureAccess represents "
+                        f"an array but other array notation is present in the "
+                        f"full access expression: '{fwriter(self)}'")
+                return ArrayType(cursor_type.intrinsic, shape)
+
+            return ArrayType(cursor_type, shape)
+
+        # We don't have an explicit array access (because `shape` is Falsey)
+        # but is the ultimate member itself an array?
+        if isinstance(cursor_type, ArrayType):
+            if not cursor.children:
+                # It is and there are no index expressions so we return the
+                # ArrayType.
+                return cursor_type
+            # We have an access to a single element of the array.
+            # Currently arrays of scalars are handled in a
+            # different way to all other types of array. Issue #1857 will
+            # fix this anomaly.
+            if isinstance(cursor_type.intrinsic, ScalarType.Intrinsic):
+                return ScalarType(cursor_type.intrinsic, cursor_type.precision)
+            return cursor_type.intrinsic
+        return cursor_type
+
 
 # For AutoAPI documentation generation
 __all__ = ['StructureReference']
diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py
index ccb1162e5..0a3095f9a 100644
--- a/src/psyclone/psyir/symbols/datatypes.py
+++ b/src/psyclone/psyir/symbols/datatypes.py
@@ -39,15 +39,14 @@
 import abc
 from collections import OrderedDict, namedtuple
 from enum import Enum
-import six
+
 from psyclone.errors import InternalError
 from psyclone.psyir.symbols.data_type_symbol import DataTypeSymbol
 from psyclone.psyir.symbols.datasymbol import DataSymbol
 from psyclone.psyir.symbols.symbol import Symbol
 
 
-@six.add_metaclass(abc.ABCMeta)
-class DataType():
+class DataType(metaclass=abc.ABCMeta):
     '''Abstract base class from which all types are derived.'''
 
     @abc.abstractmethod
@@ -58,6 +57,15 @@ class DataType():
 
         '''
 
+    def __eq__(self, other):
+        '''
+        :param Any other: the object to check equality to.
+
+        :returns: whether this type is equal to the 'other' type.
+        :rtype: bool
+        '''
+        return type(other) is type(self)
+
 
 class DeferredType(DataType):
     '''Indicates that the type is unknown at this point.'''
@@ -79,8 +87,7 @@ class NoType(DataType):
         return "NoType"
 
 
-@six.add_metaclass(abc.ABCMeta)
-class UnknownType(DataType):
+class UnknownType(DataType, metaclass=abc.ABCMeta):
     '''
     Indicates that a variable declaration is not supported by the PSyIR.
     This class is abstract and must be subclassed for each language
@@ -131,13 +138,82 @@ class UnknownFortranType(UnknownType):
 
     :param str declaration_txt: string containing the original variable \
                                 declaration.
-
-    :raises TypeError: if the supplied declaration_txt is not a str.
-
     '''
+    def __init__(self, declaration_txt):
+        super().__init__(declaration_txt)
+        # This will hold the Fortran type specification (as opposed to
+        # the whole declaration).
+        self._type_text = ""
+
     def __str__(self):
         return f"UnknownFortranType('{self._declaration}')"
 
+    @property
+    def declaration(self):
+        '''
+        This useless routine is required so that we can override the associated
+        setter method below.
+        '''
+        return super().declaration
+
+    @declaration.setter
+    def declaration(self, value):
+        '''
+        Sets the original declaration that this instance represents and
+        removes any cached type text.
+
+        :param str value: the original declaration.
+
+        '''
+        self._declaration = value[:]
+        self._type_text = ""
+
+    @property
+    def type_text(self):
+        '''
+        Parses the original Fortran declaration and uses the resulting
+        parse tree to extract the type information. This is returned in
+        text form and also cached.
+
+        TODO #1419 - alter Unknown(Fortran)Type so that it is only the
+        type information that is stored as a string. i.e. remove the name
+        of the variable being declared. Once that is done this method
+        won't be required.
+
+        :returns: the Fortran code specifying the type.
+        :rtype: str
+        '''
+        if self._type_text:
+            return self._type_text
+
+        # Encapsulate fparser2 functionality here.
+        # pylint:disable=import-outside-toplevel
+        from fparser.common.readfortran import FortranStringReader
+        from fparser.common.sourceinfo import FortranFormat
+        from fparser.two import Fortran2008
+        from fparser.two.parser import ParserFactory
+        string_reader = FortranStringReader(self._declaration)
+        # Set reader to free format.
+        string_reader.set_format(FortranFormat(True, False))
+        ParserFactory().create(std="f2008")
+        ptree = Fortran2008.Declaration_Construct(
+            string_reader)
+        self._type_text = str(ptree.children[0])
+
+        return self._type_text
+
+    def __eq__(self, other):
+        '''
+        :param Any other: the object to check equality to.
+
+        :returns: whether this type is equal to the 'other' type.
+        :rtype: bool
+        '''
+        if not super().__eq__(other):
+            return False
+
+        return other.type_text == self.type_text
+
 
 class ScalarType(DataType):
     '''Describes a scalar datatype (and its precision).
@@ -232,14 +308,13 @@ class ScalarType(DataType):
     def __eq__(self, other):
         '''
         :param Any other: the object to check equality to.
-        :returns: whether this scalar type is equal to the 'other' scalar type.
+
+        :returns: whether this type is equal to the 'other' type.
         :rtype: bool
         '''
-        # A ScalarType is not equal to e.g. an ArrayType.
-        if not type(other) is type(self):
+        if not super().__eq__(other):
             return False
-        # TODO #1799 - this method needs implementing for the other Types as
-        # currently we're not consistent.
+
         return (self.precision == other.precision and
                 self.intrinsic == other.intrinsic)
 
@@ -317,6 +392,9 @@ class ArrayType(DataType):
             if not isinstance(datatype, UnknownType):
                 self._intrinsic = datatype.intrinsic
                 self._precision = datatype.precision
+            else:
+                self._intrinsic = datatype
+                self._precision = None
         elif isinstance(datatype, DataTypeSymbol):
             self._intrinsic = datatype
             self._precision = None
@@ -418,7 +496,7 @@ class ArrayType(DataType):
             :raises TypeError: if the DataNode is not valid in this context.
 
             '''
-            # When issue #685 is addressed then check that the
+            # When issue #1799 is addressed then check that the
             # datatype returned is an int (or is unknown). For the
             # moment, just check that if the DataNode is a
             # Reference then the associated symbol is a scalar
@@ -426,16 +504,19 @@ class ArrayType(DataType):
             if isinstance(dim_node, Reference):
                 # Check the DataSymbol instance is a scalar
                 # integer or is unknown
-                symbol = dim_node.symbol
-                if not ((symbol.is_scalar and symbol.datatype.intrinsic ==
-                         ScalarType.Intrinsic.INTEGER) or
-                        isinstance(symbol.datatype,
-                                   (UnknownFortranType, DeferredType))):
+                dtype = dim_node.datatype
+                if isinstance(dtype, ArrayType) and dtype.shape:
+                    raise TypeError(
+                        f"If a DataSymbol is referenced in a dimension "
+                        f"declaration then it should be a scalar but "
+                        f"'{dim_node}' is not.")
+                if not (isinstance(dtype, (UnknownType, DeferredType)) or
+                        dtype.intrinsic == ScalarType.Intrinsic.INTEGER):
                     raise TypeError(
                         f"If a DataSymbol is referenced in a dimension "
-                        f"declaration then it should be a scalar integer or "
+                        f"declaration then it should be an integer or "
                         f"of UnknownType or DeferredType, but "
-                        f"'{symbol.name}' is a '{symbol.datatype}'.")
+                        f"'{dim_node.name}' is a '{dtype}'.")
                 # TODO #1089 - add check that any References are not to a
                 # local datasymbol that is not constant (as this would have
                 # no value).
@@ -507,6 +588,34 @@ class ArrayType(DataType):
                     f"'{type(dimension).__name__}'.")
         return f"Array<{self._datatype}, shape=[{', '.join(dims)}]>"
 
+    def __eq__(self, other):
+        '''
+        :param Any other: the object to check equality to.
+
+        :returns: whether this ArrayType is equal to the 'other' ArrayType.
+        :rtype: bool
+        '''
+        if not super().__eq__(other):
+            return False
+
+        if (self.intrinsic != other.intrinsic or
+                self.precision != other.precision):
+            return False
+
+        if len(self.shape) != len(other.shape):
+            return False
+
+        # TODO #1799 - this implementation currently has some limitations.
+        # e.g. a(1:10) and b(2:11) have the same datatype (an array of 1
+        # dimension and 10 elements) but we will currently return false.
+        # One improvement could be to use the SymbolicMath to do the comparison
+        # but this won't resolve all cases as shape can be references.
+        for this_dim, other_dim in zip(self.shape, other.shape):
+            if this_dim != other_dim:
+                return False
+
+        return True
+
 
 class StructureType(DataType):
     '''
@@ -607,6 +716,24 @@ class StructureType(DataType):
         '''
         return self._components[name]
 
+    def __eq__(self, other):
+        '''
+        :param Any other: the object to check equality to.
+
+        :returns: whether this StructureType is equal to the 'other' type.
+        :rtype: bool
+        '''
+        if not super().__eq__(other):
+            return False
+
+        if len(self.components) != len(other.components):
+            return False
+
+        if self.components != other.components:
+            return False
+
+        return True
+
 
 # Create common scalar datatypes
 REAL_TYPE = ScalarType(ScalarType.Intrinsic.REAL,


Test patch:
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py
index de81893fb..b5c255056 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py
@@ -37,7 +37,6 @@
 
 ''' Performs py.test tests on the fparser2 PSyIR front-end '''
 
-from __future__ import absolute_import
 import pytest
 
 import fparser
@@ -421,7 +420,7 @@ def test_array_notation_rank():
     # An array with no dimensions raises an exception
     array_type = ArrayType(REAL_TYPE, [10])
     symbol = DataSymbol("a", array_type)
-    array = ArrayReference(symbol, [])
+    array = ArrayReference(symbol)
     with pytest.raises(InternalError) as excinfo:
         Fparser2Reader._array_notation_rank(array)
     assert ("ArrayReference malformed or incomplete: must have one or more "
diff --git a/src/psyclone/tests/psyir/nodes/array_mixin_test.py b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
index a9eaee7d3..6896a8046 100644
--- a/src/psyclone/tests/psyir/nodes/array_mixin_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
@@ -38,8 +38,9 @@
 ''' Performs py.test tests of the ArrayMixin PSyIR nodes trait. '''
 
 import pytest
+from psyclone.errors import InternalError
 from psyclone.psyir.nodes import ArrayReference, ArrayOfStructuresReference, \
-    Range, Literal
+    BinaryOperation, Range, Literal, Routine
 from psyclone.psyir.symbols import DataSymbol, DeferredType, ArrayType, \
     INTEGER_TYPE
 
@@ -105,3 +106,49 @@ def test_is_upper_lower_bound(fortran_reader):
     array_ref = psyir.children[0].children[0].lhs
     assert not array_ref.is_lower_bound(0)
     assert not array_ref.is_upper_bound(0)
+
+
+def test_get_effective_shape(fortran_reader, fortran_writer):
+    '''Tests for the _get_effective_shape() method.'''
+    code = (
+        "subroutine test()\n"
+        "  use some_mod\n"
+        "  integer :: indices(8,3)\n"
+        "  real a(10), b(10,10)\n"
+        "  a(1:10) = 0.0\n"
+        "  b(indices(2:3,1), 2:5) = 2.0\n"
+        "  b(indices(2:3,1:2), 2:5) = 2.0\n"
+        "  a(f()) = 2.0\n"
+        "  a(2+3) = 1.0\n"
+        "end subroutine\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    routine = psyir.walk(Routine)[0]
+    # Direct array slice.
+    shape = routine.children[0].lhs._get_effective_shape()
+    assert len(shape) == 1
+    assert isinstance(shape[0], BinaryOperation)
+    # Indirect array slice.
+    shape = routine.children[1].lhs._get_effective_shape()
+    assert len(shape) == 2
+    assert isinstance(shape[0], BinaryOperation)
+    code = fortran_writer(shape[0])
+    # An ArrayType does not store the number of elements, just lower and upper
+    # bounds. Therefore, we end up recursively computing the no. of elements.
+    # The answer is still "2" though!
+    assert code == "((3 - 2) / 1 + 1 - 1) / 1 + 1"
+    code = fortran_writer(shape[1])
+    assert code == "(5 - 2) / 1 + 1"
+    # An indirect array slice can only be 1D.
+    with pytest.raises(InternalError) as err:
+        _ = routine.children[2].lhs._get_effective_shape()
+    assert ("array defining a slice of a dimension of another array must be "
+            "1D but 'indices' used to index into 'b' has 2 dimensions" in
+            str(err.value))
+    # Indirect array access using function call.
+    with pytest.raises(NotImplementedError) as err:
+        _ = routine.children[3].lhs._get_effective_shape()
+    assert "include a function call or expression" in str(err.value)
+    # Array access with expression in indices.
+    with pytest.raises(NotImplementedError) as err:
+        _ = routine.children[4].lhs._get_effective_shape()
+    assert "include a function call or expression" in str(err.value)
diff --git a/src/psyclone/tests/psyir/nodes/array_of_structures_reference_test.py b/src/psyclone/tests/psyir/nodes/array_of_structures_reference_test.py
index 8de5333f6..b55caaaaa 100644
--- a/src/psyclone/tests/psyir/nodes/array_of_structures_reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_of_structures_reference_test.py
@@ -38,7 +38,6 @@
 ''' This module contains pytest tests for the ArrayOfStructuresReference
     class. '''
 
-from __future__ import absolute_import
 import pytest
 from psyclone.tests.utilities import check_links
 from psyclone.psyir import symbols, nodes
@@ -167,3 +166,43 @@ def test_ast_is_array():
     asref = nodes.ArrayOfStructuresReference.create(
         ssym, [nodes.Literal("2", symbols.INTEGER_TYPE)], ["nx"])
     assert asref.is_array
+
+
+def test_asr_datatype():
+    '''Test that the datatype property works correctly for
+    ArrayOfStructuresReference. (The actual implementation is in
+    StructureReference.)'''
+    one = nodes.Literal("1", symbols.INTEGER_TYPE)
+    two = nodes.Literal("2", symbols.INTEGER_TYPE)
+
+    ndofs = symbols.DataSymbol("ndofs", symbols.INTEGER_TYPE)
+    atype = symbols.ArrayType(symbols.REAL_TYPE,
+                              [nodes.Reference(ndofs), nodes.Reference(ndofs)])
+    grid_type = symbols.StructureType.create([
+        ("nx", symbols.INTEGER_TYPE, symbols.Symbol.Visibility.PUBLIC),
+        ("data", atype, symbols.Symbol.Visibility.PUBLIC)])
+    grid_type_symbol = symbols.DataTypeSymbol("grid_type", grid_type)
+    grid_array_type = symbols.ArrayType(grid_type_symbol, [5])
+    ssym = symbols.DataSymbol("grid", grid_array_type)
+    # Reference to a single member of the array of structures and to the "nx"
+    # member of it.
+    asref = nodes.ArrayOfStructuresReference.create(
+        ssym, [two.copy()], ["nx"])
+    assert asref.datatype == symbols.INTEGER_TYPE
+    # Reference to a range of members of the array of structures and to the
+    # "nx" member of each.
+    my_range = nodes.Range.create(two.copy(),
+                                  nodes.Literal("3", symbols.INTEGER_TYPE))
+    asref2 = nodes.ArrayOfStructuresReference.create(
+        ssym, [my_range], ["nx"])
+    assert isinstance(asref2.datatype, symbols.ArrayType)
+    assert asref2.datatype.intrinsic == symbols.ScalarType.Intrinsic.INTEGER
+    assert len(asref2.datatype.shape) == 1
+    assert asref2.datatype.shape[0].lower == one
+    assert isinstance(asref2.datatype.shape[0].upper, nodes.BinaryOperation)
+    # Reference to a single member of the array of structures and to the "data"
+    # member of it which is itself an array.
+    asref3 = nodes.ArrayOfStructuresReference.create(
+        ssym, [one.copy()], ["data"])
+    assert isinstance(asref3.datatype, symbols.ArrayType)
+    assert len(asref3.datatype.shape) == 2
diff --git a/src/psyclone/tests/psyir/nodes/array_reference_test.py b/src/psyclone/tests/psyir/nodes/array_reference_test.py
index e4de58dee..d98dc9ae1 100644
--- a/src/psyclone/tests/psyir/nodes/array_reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_reference_test.py
@@ -38,15 +38,14 @@
 
 ''' Performs py.test tests of the ArrayReference PSyIR node. '''
 
-from __future__ import absolute_import
 import pytest
+from psyclone.errors import GenerationError, InternalError
+from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.psyir.nodes.node import colored
 from psyclone.psyir.nodes import Reference, ArrayReference, Assignment, \
     Literal, BinaryOperation, Range, KernelSchedule
-from psyclone.psyir.symbols import DataSymbol, ArrayType, \
+from psyclone.psyir.symbols import DataSymbol, ArrayType, ScalarType, \
     REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, INTEGER_TYPE
-from psyclone.errors import GenerationError, InternalError
-from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.tests.utilities import check_links
 
 
@@ -65,11 +64,14 @@ def test_array_can_be_printed():
     '''Test that an ArrayReference instance can always be printed (i.e. is
     initialised fully)'''
     kschedule = KernelSchedule("kname")
-    symbol = DataSymbol("aname", INTEGER_SINGLE_TYPE)
+    symbol = DataSymbol("aname", ArrayType(INTEGER_SINGLE_TYPE, [10]))
     kschedule.symbol_table.add(symbol)
     assignment = Assignment()
-    array = ArrayReference(symbol, assignment)
+    array = ArrayReference(symbol, parent=assignment)
     assert "ArrayReference[name:'aname']\n" in str(array)
+    array2 = ArrayReference.create(symbol, [Literal("1", INTEGER_TYPE)])
+    assert ("ArrayReference[name:'aname']\nLiteral[value:'1', "
+            "Scalar<INTEGER, UNDEFINED>]" in str(array2))
 
 
 def test_array_create():
@@ -482,3 +484,29 @@ def test_array_same_array():
     # A Reference to the array symbol should also match
     bare_array = Reference(test_sym)
     assert array.is_same_array(bare_array) is True
+
+
+def test_array_datatype(fortran_writer):
+    '''Test the datatype() method for an ArrayReference.'''
+    test_sym = DataSymbol("test", ArrayType(REAL_TYPE, [10]))
+    one = Literal("1", INTEGER_TYPE)
+    two = Literal("2", INTEGER_TYPE)
+    four = Literal("4", INTEGER_TYPE)
+    # Reference to a single element of an array.
+    aref = ArrayReference.create(test_sym, [one])
+    assert aref.datatype == REAL_TYPE
+    # Reference to a 1D sub-array of a 2D array.
+    test_sym2d = DataSymbol("test", ArrayType(REAL_TYPE, [10, 8]))
+    bref = ArrayReference.create(test_sym2d, [two.copy(),
+                                              Range.create(two.copy(),
+                                                           four.copy())])
+    assert isinstance(bref.datatype, ArrayType)
+    assert bref.datatype.intrinsic == ScalarType.Intrinsic.REAL
+    assert len(bref.datatype.shape) == 1
+    # The sub-array will have a lower bound of one.
+    assert bref.datatype.shape[0].lower == one
+    upper = bref.datatype.shape[0].upper
+    assert isinstance(upper, BinaryOperation)
+    # The easiest way to check the expression is to convert it to Fortran
+    code = fortran_writer(upper)
+    assert code == "(4 - 2) / 1 + 1"
diff --git a/src/psyclone/tests/psyir/nodes/assignment_test.py b/src/psyclone/tests/psyir/nodes/assignment_test.py
index 21c4f441b..878d8684d 100644
--- a/src/psyclone/tests/psyir/nodes/assignment_test.py
+++ b/src/psyclone/tests/psyir/nodes/assignment_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2019-2021, Science and Technology Facilities Council.
+# Copyright (c) 2019-2022, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,6 @@
 
 ''' Performs py.test tests on the Assignment PSyIR node. '''
 
-from __future__ import absolute_import
 import pytest
 from psyclone.errors import InternalError, GenerationError
 from psyclone.f2pygen import ModuleGen
@@ -78,7 +77,7 @@ def test_assignment_semantic_navigation():
     assert "' malformed or incomplete. It needs at least 1 child to have " \
         "a lhs." in str(err.value)
 
-    ref = Reference(DataSymbol("a", REAL_SINGLE_TYPE), assignment)
+    ref = Reference(DataSymbol("a", REAL_SINGLE_TYPE), parent=assignment)
     assignment.addchild(ref)
 
     # rhs should fail if second child is not present
diff --git a/src/psyclone/tests/psyir/nodes/reference_test.py b/src/psyclone/tests/psyir/nodes/reference_test.py
index e2f3bc7c5..7354ac754 100644
--- a/src/psyclone/tests/psyir/nodes/reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/reference_test.py
@@ -39,15 +39,16 @@
 
 ''' Performs py.test tests on the Reference PSyIR node. '''
 
-from __future__ import absolute_import
 import pytest
-from psyclone.psyir.nodes import Reference, ArrayReference, Assignment, \
-    Literal, BinaryOperation, Range, KernelSchedule
-from psyclone.psyir.symbols import DataSymbol, ArrayType, \
-    REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, INTEGER_TYPE
-from psyclone.psyGen import GenerationError
 from psyclone.core.access_info import VariablesAccessInfo
+from psyclone.psyGen import GenerationError
+from psyclone.psyir.nodes import (
+    Reference, ArrayReference, Assignment,
+    Literal, BinaryOperation, Range, KernelSchedule)
 from psyclone.psyir.nodes.node import colored
+from psyclone.psyir.symbols import (
+    DataSymbol, ArrayType, ScalarType,
+    REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, INTEGER_TYPE)
 
 
 def test_reference_bad_init():
@@ -91,7 +92,7 @@ def test_reference_node_str():
     symbol = DataSymbol("rname", INTEGER_SINGLE_TYPE)
     kschedule.symbol_table.add(symbol)
     assignment = Assignment()
-    ref = Reference(symbol, assignment)
+    ref = Reference(symbol, parent=assignment)
     coloredtext = colored("Reference", Reference._colour)
     assert coloredtext+"[name:'rname']" in ref.node_str()
 
@@ -103,7 +104,7 @@ def test_reference_can_be_printed():
     symbol = DataSymbol("rname", INTEGER_SINGLE_TYPE)
     kschedule.symbol_table.add(symbol)
     assignment = Assignment()
-    ref = Reference(symbol, assignment)
+    ref = Reference(symbol, parent=assignment)
     assert "Reference[name:'rname']" in str(ref)
 
 
@@ -135,6 +136,15 @@ def test_reference_is_array():
     assert reference.is_array is False
 
 
+def test_reference_datatype():
+    '''Test the datatype property.
+
+    '''
+    reference = Reference(DataSymbol("test", REAL_TYPE))
+    assert isinstance(reference.datatype, ScalarType)
+    assert reference.datatype.intrinsic == ScalarType.Intrinsic.REAL
+
+
 def test_reference_accesses():
     '''Test that the reference_accesses method behaves as expected in the
     usual case (see the next test for the unusual case).
diff --git a/src/psyclone/tests/psyir/nodes/structure_reference_test.py b/src/psyclone/tests/psyir/nodes/structure_reference_test.py
index ddae1e932..e95f9734f 100644
--- a/src/psyclone/tests/psyir/nodes/structure_reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/structure_reference_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2021, Science and Technology Facilities Council.
+# Copyright (c) 2020-2022, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -38,8 +38,6 @@
 
 ''' Module containing pytest tests for the StructureReference class. '''
 
-from __future__ import absolute_import
-
 import pytest
 
 from psyclone.core import Signature, VariablesAccessInfo
@@ -204,3 +202,123 @@ def test_struc_ref_semantic_nav():
     assert ("StructureReference malformed or incomplete. It must have a "
             "single child that must be a (sub-class of) Member, but "
             "found: ['broken']" in str(err.value))
+
+
+def test_struc_ref_datatype():
+    '''Test the datatype() method of StructureReference.'''
+    atype = symbols.ArrayType(symbols.REAL_TYPE, [10, 8])
+    rtype = symbols.StructureType.create([
+        ("gibber", symbols.BOOLEAN_TYPE, symbols.Symbol.Visibility.PUBLIC)])
+    # TODO #1031. Currently cannot create an array of StructureTypes
+    # directly - have to have a DataTypeSymbol.
+    rtype_sym = symbols.DataTypeSymbol("gibber_type", rtype)
+    artype = symbols.ArrayType(rtype_sym, [10, 3])
+    grid_type = symbols.StructureType.create([
+        ("nx", symbols.INTEGER_TYPE, symbols.Symbol.Visibility.PUBLIC),
+        ("data", atype, symbols.Symbol.Visibility.PRIVATE),
+        ("roger", rtype, symbols.Symbol.Visibility.PUBLIC),
+        ("titty", artype, symbols.Symbol.Visibility.PUBLIC)])
+    # Symbol with type defined by StructureType
+    ssym0 = symbols.DataSymbol("grid", grid_type)
+    # Reference to scalar member of structure
+    sref0 = nodes.StructureReference.create(ssym0, ["nx"])
+    assert sref0.datatype == symbols.INTEGER_TYPE
+
+    # Symbol with type defined by DataTypeSymbol
+    grid_type_symbol = symbols.DataTypeSymbol("grid_type", grid_type)
+    ssym = symbols.DataSymbol("grid", grid_type_symbol)
+    # Reference to scalar member of structure
+    sref = nodes.StructureReference.create(ssym, ["nx"])
+    assert sref.datatype == symbols.INTEGER_TYPE
+    one = nodes.Literal("1", symbols.INTEGER_TYPE)
+    two = nodes.Literal("2", symbols.INTEGER_TYPE)
+    sref2 = nodes.StructureReference.create(ssym, [("data", [one, two])])
+    assert sref2.datatype == symbols.REAL_TYPE
+
+    # Reference to scalar member of structure member
+    gref = nodes.StructureReference.create(ssym, ["roger", "gibber"])
+    assert gref.datatype == symbols.BOOLEAN_TYPE
+
+    # Reference to structure member of structure
+    rref = nodes.StructureReference.create(ssym, ["roger"])
+    assert rref.datatype == rtype
+
+    # Reference to single element of array of structures within a structure
+    singleref = nodes.StructureReference.create(
+        ssym, [("titty", [one.copy(), two.copy()])])
+    assert singleref.datatype == rtype_sym
+
+    # Reference to sub-array of structure members of structure
+    myrange = nodes.Range.create(two.copy(),
+                                 nodes.Literal("4", symbols.INTEGER_TYPE))
+    arref = nodes.StructureReference.create(
+        ssym, [("titty", [nodes.Literal("3", symbols.INTEGER_TYPE), myrange])])
+    dtype = arref.datatype
+    assert isinstance(dtype, symbols.ArrayType)
+    assert dtype.intrinsic == rtype_sym
+    assert len(dtype.shape) == 1
+    assert dtype.shape[0].lower == one
+    assert isinstance(dtype.shape[0].upper, nodes.BinaryOperation)
+
+    # Reference to whole array of structures that are a member of a structure
+    fullref = nodes.StructureReference.create(ssym, ["titty"])
+    dtype = fullref.datatype
+    assert dtype == artype
+
+
+def test_structure_reference_deferred_type():
+    '''
+    Check that the datatype() method behaves as expected when it
+    encounters members of DeferredType or UnknownType.
+
+    '''
+    atype = symbols.ArrayType(
+        symbols.UnknownFortranType(
+            "type(atype), dimension(10,8), pointer :: aptr"), [10, 8])
+    grid_type = symbols.StructureType.create([
+        ("mesh", symbols.DeferredType(), symbols.Symbol.Visibility.PUBLIC),
+        ("aptr", atype, symbols.Symbol.Visibility.PUBLIC)])
+    grid_type_symbol = symbols.DataTypeSymbol("grid_type", grid_type)
+    ssym = symbols.DataSymbol("grid", grid_type_symbol)
+    # Structure of deferred type
+    deft_sym = symbols.DataSymbol("john", symbols.DeferredType())
+    jref = nodes.StructureReference.create(deft_sym, ["value"])
+    assert jref.datatype == symbols.DeferredType()
+    # Structure of UnknownType
+    ut_sym = symbols.DataSymbol("teasel",
+                                symbols.UnknownFortranType("some type decln"))
+    tref = nodes.StructureReference.create(ut_sym, ["yard"])
+    assert tref.datatype == symbols.DeferredType()
+    # Structure with type given by DataTypeSymbol that is of DeferredType
+    utypesym = symbols.DataTypeSymbol("my_type", symbols.DeferredType())
+    mysym = symbols.DataSymbol("my_sym", utypesym)
+    myref = nodes.StructureReference.create(mysym, ["flag"])
+    assert myref.datatype == symbols.DeferredType()
+    # Member of structure that is of deferred type
+    meshref = nodes.StructureReference.create(ssym, ["mesh", "polly"])
+    assert meshref.datatype == symbols.DeferredType()
+    # Member of structure that is an array of unknown type.
+    two = nodes.Literal("2", symbols.INTEGER_TYPE)
+    four = nodes.Literal("4", symbols.INTEGER_TYPE)
+    myrange = nodes.Range.create(two.copy(), four.copy())
+    aref = nodes.StructureReference.create(ssym, [("aptr",
+                                                   [two.copy(), myrange])])
+    assert len(aref.datatype.shape) == 1
+    assert isinstance(aref.datatype, symbols.ArrayType)
+    assert isinstance(aref.datatype.intrinsic, symbols.UnknownFortranType)
+    # An array made of individual elements of a member array.
+    # my_sym(:)%aptr(2,2)
+    array_grid_type = symbols.ArrayType(grid_type_symbol, [four.copy()])
+    array_sym = symbols.DataSymbol("thing", array_grid_type)
+    aref2 = nodes.ArrayOfStructuresReference.create(
+        array_sym, [myrange.copy()],
+        [("aptr", [two.copy(), two.copy()])])
+    assert len(aref2.datatype.shape) == 1
+    assert isinstance(aref2.datatype.intrinsic, symbols.UnknownFortranType)
+    # An array of arrays - not supported.
+    # my_sym(2:4)%aptr
+    aref3 = nodes.ArrayOfStructuresReference.create(
+        array_sym, [myrange.copy()], ["aptr"])
+    with pytest.raises(NotImplementedError) as err:
+        _ = aref3.datatype
+    assert "Array of arrays not supported: " in str(err.value)
diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py
index 082154f0c..aa56731bc 100644
--- a/src/psyclone/tests/psyir/symbols/datatype_test.py
+++ b/src/psyclone/tests/psyir/symbols/datatype_test.py
@@ -74,6 +74,13 @@ def test_deferredtype_str():
     assert str(data_type) == "DeferredType"
 
 
+def test_deferredtype_eq():
+    '''Test the equality operator of DeferredType.'''
+    data_type1 = DeferredType()
+    assert data_type1 == DeferredType()
+    assert data_type1 != NoType()
+
+
 # NoType class
 
 def test_notype():
@@ -84,6 +91,15 @@ def test_notype():
     assert str(data_type) == "NoType"
 
 
+def test_notype_eq():
+    '''Test the equality operator of NoType.'''
+    notype1 = NoType()
+    assert notype1 == NoType()
+    assert notype1 != DeferredType()
+    assert notype1 != ScalarType(ScalarType.Intrinsic.INTEGER,
+                                 ScalarType.Precision.SINGLE)
+
+
 # ScalarType class
 
 @pytest.mark.parametrize("precision", [ScalarType.Precision.SINGLE,
@@ -173,7 +189,7 @@ def test_scalartype_not_equal():
     assert scalar_type4 != scalar_type
     # A ScalarType is not equal to an ArrayType
     atype = ArrayType(scalar_type4, [10])
-    assert atype != scalar_type4
+    assert scalar_type4 != atype
 
 
 def test_scalartype_invalid_intrinsic_type():
@@ -352,7 +368,7 @@ def test_arraytype_invalid_shape_dimension_1():
         _ = ArrayType(scalar_type, [Reference(symbol)])
     assert (
         "If a DataSymbol is referenced in a dimension declaration then it "
-        "should be a scalar integer or of UnknownType or DeferredType, but "
+        "should be an integer or of UnknownType or DeferredType, but "
         "'fred' is a 'Scalar<REAL, 4>'." in str(excinfo.value))
 
 
@@ -394,7 +410,7 @@ def test_arraytype_invalid_shape_dimension_3():
 def test_arraytype_invalid_shape_bounds():
     ''' Check that the ArrayType class raises the expected exception when
     one of the dimensions of the shape list is a tuple that does not contain
-    either an int or a DataNode.'''
+    either an int or a DataNode or is not a scalar.'''
     scalar_type = ScalarType(ScalarType.Intrinsic.REAL, 4)
     with pytest.raises(TypeError) as excinfo:
         _ = ArrayType(scalar_type, [(1, 4, 1)])
@@ -422,8 +438,15 @@ def test_arraytype_invalid_shape_bounds():
         _ = ArrayType(scalar_type, [(1, Reference(symbol))])
     assert (
         "If a DataSymbol is referenced in a dimension declaration then it "
-        "should be a scalar integer or of UnknownType or DeferredType, but "
+        "should be an integer or of UnknownType or DeferredType, but "
         "'fred' is a 'Scalar<REAL, 4>'." in str(excinfo.value))
+    array_type = ArrayType(INTEGER_TYPE, [10])
+    symbol = DataSymbol("jim", array_type)
+    with pytest.raises(TypeError) as excinfo:
+        _ = ArrayType(scalar_type, [(1, Reference(symbol))])
+    assert ("If a DataSymbol is referenced in a dimension declaration then it "
+            "should be a scalar but 'Reference[name:'jim']' is not." in
+            str(excinfo.value))
 
 
 def test_arraytype_shape_dim_from_parent_scope():
@@ -483,6 +506,30 @@ def test_arraytype_immutable():
         data_type.shape = []
 
 
+def test_arraytype_eq():
+    '''Test the equality operator for ArrayType.'''
+    scalar_type = ScalarType(ScalarType.Intrinsic.REAL, 4)
+    data_type1 = ArrayType(scalar_type, [10, 10])
+    assert data_type1 == ArrayType(scalar_type, [10, 10])
+    assert data_type1 != scalar_type
+    assert data_type1 == ArrayType(scalar_type, [10,
+                                                 Literal("10", INTEGER_TYPE)])
+    # Same type but different shape.
+    assert data_type1 != ArrayType(scalar_type, [10])
+    assert data_type1 != ArrayType(scalar_type, [10, 10, 5])
+    assert data_type1 != ArrayType(scalar_type, [10, 5])
+    assert data_type1 != ArrayType(scalar_type, [10, 5])
+    sym = DataSymbol("nx", INTEGER_TYPE)
+    assert data_type1 != ArrayType(scalar_type, [10, Reference(sym)])
+    # Same shape but different type.
+    dscalar_type = ScalarType(ScalarType.Intrinsic.REAL, 8)
+    assert data_type1 != ArrayType(dscalar_type, [10, 10])
+    iscalar_type = ScalarType(ScalarType.Intrinsic.INTEGER, 4)
+    assert data_type1 != ArrayType(iscalar_type, [10, 10])
+
+
+# UnknownFortranType tests
+
 def test_unknown_fortran_type():
     ''' Check the constructor and 'declaration' property of the
     UnknownFortranType class. '''
@@ -496,6 +543,34 @@ def test_unknown_fortran_type():
     assert utype.declaration == decl
 
 
+def test_unknown_fortran_type_text():
+    '''
+    Check that the 'type_text' property returns the expected string and
+    that the result is cached.
+    '''
+    decl = "type(some_type) :: var"
+    utype = UnknownFortranType(decl)
+    text = utype.type_text
+    assert text == "TYPE(some_type)"
+    # Calling it a second time should just return the previously cached
+    # result.
+    assert utype.type_text is text
+    # Changing the declaration text should wipe the cache
+    utype.declaration = decl
+    assert utype.type_text is not text
+
+
+def test_unknown_fortran_type_eq():
+    '''Test the equality operator for UnknownFortranType.'''
+    decl = "type(some_type) :: var"
+    utype = UnknownFortranType(decl)
+    assert utype == UnknownFortranType(decl)
+    assert utype != NoType()
+    # Type is the same even if the variable name is different.
+    assert utype == UnknownFortranType("type(some_type) :: var1")
+    assert utype != UnknownFortranType("type(other_type) :: var")
+
+
 # StructureType tests
 
 def test_structure_type():
@@ -552,3 +627,32 @@ def test_create_structuretype():
     assert ("Each component must be specified using a 3-tuple of (name, "
             "type, visibility) but found a tuple with 2 members: ("
             "'george', " in str(err.value))
+
+
+def test_structuretype_eq():
+    '''Test the equality operator of StructureType.'''
+    stype = StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE)])
+    assert stype == StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE)])
+    # Something that is not a StructureType
+    assert stype != NoType()
+    # Component with a different name.
+    assert stype != StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("roger", REAL_TYPE, Symbol.Visibility.PRIVATE)])
+    # Component with a different type.
+    assert stype != StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("peggy", INTEGER_TYPE, Symbol.Visibility.PRIVATE)])
+    # Component with a different visibility.
+    assert stype != StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("peggy", REAL_TYPE, Symbol.Visibility.PUBLIC)])
+    # Different number of components.
+    assert stype != StructureType.create([
+        ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC),
+        ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE),
+        ("roger", INTEGER_TYPE, Symbol.Visibility.PUBLIC)])