Repository: stfc/PSyclone
Pull request #2387 (2023-11-03 16:22:00)

(Towards #1799) Support querying the datatype of a unary/binary operation.


Patch:
diff --git a/changelog b/changelog
index b1c705399..98ef153e3 100644
--- a/changelog
+++ b/changelog
@@ -97,6 +97,9 @@
 
 	36) PR #2401 for #2261. Split DynLoop out of dynamo0p3 and rename.
 
+	37) PR #2387 towards #1799. Add support for querying the datatype of
+	operations.
+
 release 2.4.0 29th of September 2023
 
 	1) PR #1758 for #1741. Splits the PSyData read functionality into a
diff --git a/doc/developer_guide/psyir.rst b/doc/developer_guide/psyir.rst
index 4507d71dd..b91cafa72 100644
--- a/doc/developer_guide/psyir.rst
+++ b/doc/developer_guide/psyir.rst
@@ -521,7 +521,40 @@ sections of the reference guide.
         datatype of the operands (e.g. in the select-case canonicalisation).
         A solution to this is to create an abstract interface with appropriate
         implementations for each possible datatype.
-            
+
+Data Type of an Operation Node
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Table 7.2 of the Fortran2008 standard specifies the rules governing
+the types of operands and their results. The PSyIR follows these rules
+with the exception that there is no support for symbols of complex
+(imaginary) type (see
+`#1590 <https://github.com/stfc/PSyclone/issues/1590>`_). For unary operations,
+the type of the result is just that of the operand.  For a numeric,
+binary operation, these rules boil down to saying that if either argument
+is real then the result is real but if both arguments are integer then the
+result is integer. 
+
+If the precisions of the operands are the same, then the result must
+also be of that precision. Otherwise, Section 7.1.9.3 of the Fortran2008
+standard says that the precision of the result is the greater of the two.
+In the PSyIR, if both precisions are instances of `ScalarType.Precision` or
+`int` then this permits the precision of the result to be determined.
+Otherwise, the result is given a precision of `ScalarType.Precision.UNDEFINED`.
+
+For comparison operations (e.g. `<`, `==`), the intrinsic type of the
+result is always boolean. If either or both operands are arrays, then
+the result is a boolean array.
+
+The PSyIR type system includes support for those situations where
+PSyclone is not able to fully understand a variable declaration.
+In such cases, the type is an instance of `UnknownFortranType` which
+stores both the original declaration and, optionally, a `partial_datatype`
+holding the aspects of the type that can be represented in the PSyIR.
+The presence of a `partial_datatype` implies that we fully understand
+the intrinsic type. Given this and an array shape, it is always possible
+to determine the result of a numerical operation involving such a type.
+
 
 IntrinsicCall Nodes
 -------------------
diff --git a/doc/developer_guide/psyir_symbols.rst b/doc/developer_guide/psyir_symbols.rst
index 96d5ab5ad..52353d723 100644
--- a/doc/developer_guide/psyir_symbols.rst
+++ b/doc/developer_guide/psyir_symbols.rst
@@ -69,6 +69,54 @@ original, unsupported symbol declaration, it is subclassed for each
 language for which a PSyIR frontend exists. Currently therefore this
 is limited to ``UnknownFortranType``.
 
+The support for Fortran declaration constructs in the ``fparser2``
+frontend is summarised in the following table (any attributes not
+explicitly listed may be assumed to be unsupported):
+
+.. tabularcolumns:: |l|L|L|
+
++----------------------+--------------------+--------------------+
+|                      |Supported           |Unsupported         |
++======================+====================+====================+
+|Variables             |ALLOCATABLE         |CLASS               |
++----------------------+--------------------+--------------------+
+|                      |CHARACTER, DOUBLE   |COMPLEX, CHARACTER  |
+|                      |PRECISION, INTEGER, |with LEN or KIND    |
+|                      |LOGICAL, REAL       |                    |
++----------------------+--------------------+--------------------+
+|                      |Derived Types       |'extends',          |
+|                      |                    |'abstract' or with  |
+|                      |                    |CONTAINS; Operator  |
+|                      |                    |overloading         |
++----------------------+--------------------+--------------------+
+|                      |DIMENSION           |Array extents       |
+|                      |                    |specified using     |
+|                      |                    |expressions;        |
+|                      |                    |Assumed-size arrays |
++----------------------+--------------------+--------------------+
+|                      |Initialisation      |                    |
+|                      |expressions         |                    |
++----------------------+--------------------+--------------------+
+|                      |INTENT, PARAMETER,  |VOLATILE, VALUE,    |
+|                      |SAVE                |POINTER             |
++----------------------+--------------------+--------------------+
+|                      |KIND=param, REAL*8  |                    |
+|                      |etc.                |                    |
++----------------------+--------------------+--------------------+
+|                      |PUBLIC, PRIVATE     |                    |
++----------------------+--------------------+--------------------+
+|Imports/globals       |USE with ONLY and   |User-defined        |
+|                      |renaming            |operators           |
+|                      |                    |                    |
++----------------------+--------------------+--------------------+
+|                      |Common blocks       |                    |
+|                      |(limited)           |                    |
++----------------------+--------------------+--------------------+
+|Routine Interfaces    |PURE, IMPURE,       |CONTAINS            |
+|                      |ELEMENTAL, PUBLIC,  |                    |
+|                      |PRIVATE             |                    |
++----------------------+--------------------+--------------------+
+
 .. warning:: Checking for equality between Type objects is currently
 	     only implemented for ``ScalarType``. This will be
 	     completed in #1799.
diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py
index 9e20d3012..a5038813b 100644
--- a/src/psyclone/domain/lfric/lfric_builtins.py
+++ b/src/psyclone/domain/lfric/lfric_builtins.py
@@ -2738,7 +2738,7 @@ class LFRicIntXKern(LFRicBuiltIn):
 
         # Create the PSyIR for the kernel:
         #      proxy0%data(df) = INT(proxy0%data, kind=i_<prec>)
-        i_precision = arg_refs[0].datatype.partial_datatype.precision
+        i_precision = arg_refs[0].datatype.precision
         rhs = IntrinsicCall.create(
             IntrinsicCall.Intrinsic.INT,
             [arg_refs[1], ("kind", Reference(i_precision))])
@@ -3081,7 +3081,7 @@ class LFRicRealXKern(LFRicBuiltIn):
 
         # Create the PSyIR for the kernel:
         #      proxy0%data(df) = REAL(proxy0%data, kind=r_<prec>)
-        r_precision = arg_refs[0].datatype.partial_datatype.precision
+        r_precision = arg_refs[0].datatype.precision
         rhs = IntrinsicCall.create(
             IntrinsicCall.Intrinsic.REAL,
             [arg_refs[1], ("kind", Reference(r_precision))])
diff --git a/src/psyclone/psyir/nodes/array_reference.py b/src/psyclone/psyir/nodes/array_reference.py
index e234623fc..cfe62844c 100644
--- a/src/psyclone/psyir/nodes/array_reference.py
+++ b/src/psyclone/psyir/nodes/array_reference.py
@@ -44,7 +44,8 @@ from psyclone.psyir.nodes.literal import Literal
 from psyclone.psyir.nodes.intrinsic_call import IntrinsicCall
 from psyclone.psyir.nodes.ranges import Range
 from psyclone.psyir.nodes.reference import Reference
-from psyclone.psyir.symbols import (DataSymbol, DeferredType, UnknownType,
+from psyclone.psyir.symbols import (DataSymbol, DeferredType,
+                                    UnknownFortranType, UnknownType,
                                     DataTypeSymbol, ScalarType, ArrayType,
                                     INTEGER_TYPE)
 
@@ -128,9 +129,37 @@ class ArrayReference(ArrayMixin, Reference):
         '''
         shape = self._get_effective_shape()
         if shape:
-            return ArrayType(self.symbol.datatype, shape)
+            if isinstance(self.symbol.datatype, UnknownType):
+                # Even if an Unknown(Fortran)Type has partial type
+                # information, we can't easily use it here because we'd need
+                # to re-write the original Fortran declaration stored in the
+                # type. We could manipulate the shape in the fparser2 parse
+                # tree if need be but, at this point, we wouldn't know what
+                # the variable name should be (TODO #2137).
+                base_type = DeferredType()
+            else:
+                base_type = self.symbol.datatype
+            # TODO #1857 - passing base_type as an instance of ArrayType
+            # only works because the ArrayType constructor just pulls out
+            # the intrinsic and precision properties of the type.
+            return ArrayType(base_type, shape)
+
+        # Otherwise, we're accessing a single element of the array.
         if isinstance(self.symbol.datatype, UnknownType):
-            return self.symbol.datatype
+            if (isinstance(self.symbol.datatype, UnknownFortranType) and
+                    self.symbol.datatype.partial_datatype):
+                precision = self.symbol.datatype.partial_datatype.precision
+                intrinsic = self.symbol.datatype.partial_datatype.intrinsic
+                return ScalarType(intrinsic, precision)
+            else:
+                # Since we're accessing a single element of an array
+                # of UnknownType we have to create a new
+                # UnknownFortranType.  Ideally we would re-write the
+                # original Fortran declaration stored in the type. We
+                # could remove the shape in the fparser2 parse tree
+                # but, at this point, we wouldn't know what the
+                # variable name should be (TODO #2137).
+                return DeferredType()
         if isinstance(self.symbol.datatype.intrinsic, DataTypeSymbol):
             return self.symbol.datatype.intrinsic
         # TODO #1857: Really we should just be able to return
diff --git a/src/psyclone/psyir/nodes/datanode.py b/src/psyclone/psyir/nodes/datanode.py
index f5923ce5e..e5e927934 100644
--- a/src/psyclone/psyir/nodes/datanode.py
+++ b/src/psyclone/psyir/nodes/datanode.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020, Science and Technology Facilities Council.
+# Copyright (c) 2020-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -31,16 +31,27 @@
 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 # -----------------------------------------------------------------------------
-# Author S. Siso, STFC Daresbury Lab
+# Authors: A. R. Porter and S. Siso, STFC Daresbury Lab
 # -----------------------------------------------------------------------------
 
 ''' This module contains the DataNode abstract node implementation.'''
 
 from psyclone.psyir.nodes.node import Node
+from psyclone.psyir.symbols.datatypes import DeferredType
 
 
 class DataNode(Node):
     '''
     Abstract node representing a general PSyIR expression that represents a
     value, which has a datatype.
+
     '''
+    @property
+    def datatype(self):
+        '''
+        :returns: the data-type of this Node. Currently this base
+            implementation just returns DeferredType(). If a sub-class can do
+            better then it must override this method.
+        :rtype: :py:class:`psyclone.psyir.symbols.DeferredType`
+        '''
+        return DeferredType()
diff --git a/src/psyclone/psyir/nodes/operation.py b/src/psyclone/psyir/nodes/operation.py
index 8a6978bf4..03af66730 100644
--- a/src/psyclone/psyir/nodes/operation.py
+++ b/src/psyclone/psyir/nodes/operation.py
@@ -42,8 +42,11 @@ sub-classes.'''
 from abc import ABCMeta
 from enum import Enum
 
-from psyclone.errors import GenerationError
+from psyclone.errors import GenerationError, InternalError
 from psyclone.psyir.nodes.datanode import DataNode
+from psyclone.psyir.symbols.datatypes import (
+    ArrayType, BOOLEAN_TYPE, DeferredType, ScalarType,
+    UnknownFortranType, UnknownType)
 
 
 class Operation(DataNode, metaclass=ABCMeta):
@@ -144,6 +147,9 @@ class UnaryOperation(Operation):
         'NOT',
         ])
 
+    # The numeric operators.
+    _numeric_ops = (Operator.MINUS, Operator.PLUS)
+
     @staticmethod
     def _validate_child(position, child):
         '''
@@ -187,6 +193,15 @@ class UnaryOperation(Operation):
         unary_op.addchild(operand)
         return unary_op
 
+    @property
+    def datatype(self):
+        '''
+        :returns: the datatype of the result of this UnaryOperation.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+
+        '''
+        return self.children[0].datatype
+
 
 class BinaryOperation(Operation):
     '''
@@ -202,6 +217,9 @@ class BinaryOperation(Operation):
         # Logical Operators
         'AND', 'OR', 'EQV', 'NEQV',
         ])
+    # The numeric operators.
+    _numeric_ops = (Operator.ADD, Operator.SUB, Operator.MUL, Operator.DIV,
+                    Operator.REM, Operator.POW)
     # Textual description of the node.
     _children_valid_format = "DataNode, DataNode"
 
@@ -224,38 +242,210 @@ class BinaryOperation(Operation):
         child instances with optional names.
 
         :param operator: the operator used in the operation.
-        :type operator: \
+        :type operator:
             :py:class:`psyclone.psyir.nodes.BinaryOperation.Operator`
-        :param lhs: the PSyIR node containing the left hand side of \
-            the assignment, or a tuple containing the name of the \
+        :param lhs: the PSyIR node containing the left hand side of
+            the assignment, or a tuple containing the name of the
             argument and the PSyIR node.
-        :type lhs: Union[:py:class:`psyclone.psyir.nodes.Node`, \
+        :type lhs: Union[:py:class:`psyclone.psyir.nodes.Node`,
             Tuple[str, :py:class:`psyclone.psyir.nodes.Node`]]
-        :param rhs: the PSyIR node containing the right hand side of \
-            the assignment, or a tuple containing the name of the \
+        :param rhs: the PSyIR node containing the right hand side of
+            the assignment, or a tuple containing the name of the
             argument and the PSyIR node.
-        :type rhs: Union[:py:class:`psyclone.psyir.nodes.Node`, \
+        :type rhs: Union[:py:class:`psyclone.psyir.nodes.Node`,
             Tuple[str, :py:class:`psyclone.psyir.nodes.Node`]]
 
         :returns: a BinaryOperator instance.
         :rtype: :py:class:`psyclone.psyir.nodes.BinaryOperation`
 
-        :raises GenerationError: if the arguments to the create method \
+        :raises GenerationError: if the arguments to the create method
             are not of the expected type.
 
         '''
-        if not isinstance(operator, Enum) or \
-           operator not in BinaryOperation.Operator:
+        if (not isinstance(operator, Enum) or
+                operator not in BinaryOperation.Operator):
             raise GenerationError(
                 f"operator argument in create method of BinaryOperation class "
                 f"should be a PSyIR BinaryOperation Operator but found "
                 f"'{type(operator).__name__}'.")
-
         binary_op = BinaryOperation(operator)
         binary_op.addchild(lhs)
         binary_op.addchild(rhs)
         return binary_op
 
+    def _get_result_precision(self, precisions):
+        '''
+        Compares the two precisions to determine the precision of the result
+        of the operation.
+
+        If the two precisions are the same, then that value is returned.
+        Otherwise, Section 7.1.9.3 of the Fortran2008 standard says that in
+        this case, the precision of the result is the greater of the two.
+        If the precision cannot be determined then
+        `ScalarType.Precision.UNDEFINED` is returned.
+
+        :param precisions: the precision of the two operands.
+        :type precisions: list[int |
+            :py:class:`psyclone.psyir.symbols.ScalarType.Precision |
+            :py:class:`psyclone.psyir.nodes.Reference`]
+
+        :returns: the precision of the result of the operation.
+        :rtype: int | :py:class:`psyclone.psyir.symbols.ScalarType.Precision
+
+        :raises InternalError: if an unsupported Precision value is encountered
+            (this is to defend against any future extension of
+            ScalarType.Precision).
+
+        '''
+        if precisions[0] == precisions[1]:
+            return precisions[0]
+
+        # Operands have different precisions.
+        if all(isinstance(prec, int) for prec in precisions):
+            # Both precisions are integer.
+            return max(precisions)
+
+        if all(isinstance(prec, ScalarType.Precision) for
+               prec in precisions):
+            # Both precisions are of ScalarType.Precision type.
+            if ScalarType.Precision.UNDEFINED in precisions:
+                return ScalarType.Precision.UNDEFINED
+            if ScalarType.Precision.DOUBLE in precisions:
+                return ScalarType.Precision.DOUBLE
+            raise InternalError(
+                f"Operation._get_result_precision: got unsupported Precision "
+                f"value(s) '{precisions[0]}' and '{precisions[1]}' for "
+                f"operands '{self.children[0].debug_string()}' and "
+                f"'{self.children[1].debug_string()}'")
+
+        # We can't reason about the precision of the result.
+        return ScalarType.Precision.UNDEFINED
+
+    def _get_result_scalar_type(self, argtypes):
+        '''
+        Examines the two operand types to determine the base type of the
+        operation using the rules in Section 7.2 of the Fortran2008 standard.
+        If the type cannot be determined then an instance of `DeferredType` is
+        returned.
+
+        :param argtypes: the types of the two operands.
+        :type argtypes: list[:py:class:`psyclone.psyir.symbols.DataType`,
+                             :py:class:`psyclone.psyir.symbols.DataType`]
+
+        :returns: the base type of the result of the operation.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+
+        :raises TypeError: if an unexpected intrinsic type is found for
+                           either of the operands to a numeric operation.
+
+        '''
+        if self.operator not in self._numeric_ops:
+            # Must be a relational or logical operator. Intrinsic type of
+            # result will be boolean.
+            return BOOLEAN_TYPE
+
+        # We have a numerical operation.
+        if any(isinstance(atype.intrinsic, DeferredType)
+               for atype in argtypes):
+            # datatype of a numerical operation on a DeferredType is a
+            # DeferredType.
+            return DeferredType()
+
+        base_type = None
+
+        # If either of the operands has REAL intrinsic type then the result
+        # must also be REAL.
+        if argtypes[0].intrinsic == argtypes[1].intrinsic:
+            # Operands are of the same intrinsic type.
+            precision = self._get_result_precision([argtypes[0].precision,
+                                                    argtypes[1].precision])
+            base_type = ScalarType(argtypes[0].intrinsic, precision)
+        elif argtypes[0].intrinsic == ScalarType.Intrinsic.REAL:
+            base_type = argtypes[0]
+        elif argtypes[1].intrinsic == ScalarType.Intrinsic.REAL:
+            base_type = argtypes[1]
+
+        # Check that the type of the result is consistent with a numerical
+        # operation.
+        if not base_type or base_type.intrinsic not in (
+                ScalarType.Intrinsic.INTEGER,
+                ScalarType.Intrinsic.REAL):
+            for atype in argtypes:
+                if atype.intrinsic not in (ScalarType.Intrinsic.INTEGER,
+                                           ScalarType.Intrinsic.REAL):
+                    raise TypeError(
+                        f"Invalid argument of type '{atype.intrinsic}' to "
+                        f"numerical operation '{self.operator}' in "
+                        f"'{self.debug_string()}'. Currently only "
+                        f"ScalarType.Intrinsic.REAL/INTEGER are "
+                        f"supported (TODO #1590)")
+        return base_type
+
+    @property
+    def datatype(self):
+        '''
+        Determines the datatype of this operation. If it cannot be determined
+        for any reason then an instance of DeferredType is returned.
+
+        :returns: the datatype of the result of this BinaryOperation.
+        :rtype: :py:class:`psyclone.psyir.symbols.DataType`
+
+        :raises InternalError: if the operands are both arrays but are of
+                               different shapes.
+        '''
+        # Get the types of the operands.
+        argtypes = []
+        for child in self.children:
+            # If the operand is itself an operation this will recurse.
+            dtype = child.datatype
+            if isinstance(dtype, UnknownFortranType):
+                if dtype.partial_datatype:
+                    # We are still OK provided we have partial type information
+                    # since that means the intrinsic type can be handled in the
+                    # PSyIR.
+                    dtype = dtype.partial_datatype
+                else:
+                    return DeferredType()
+            if isinstance(dtype, DeferredType):
+                # If either operand is of DeferredType then we can't do
+                # any better.
+                return DeferredType()
+            if isinstance(dtype, ArrayType):
+                # We know this is an array but do we know its intrinsic type?
+                if isinstance(dtype.intrinsic, DeferredType):
+                    dtype = ArrayType(DeferredType(), shape=dtype.shape)
+                if isinstance(dtype.intrinsic, UnknownType):
+                    if (isinstance(dtype.intrinsic, UnknownFortranType) and
+                            dtype.intrinsic.partial_datatype):
+                        dtype = ArrayType(dtype.intrinsic.partial_datatype,
+                                          shape=dtype.shape)
+                    else:
+                        dtype = ArrayType(DeferredType(), shape=dtype.shape)
+
+            argtypes.append(dtype)
+
+        # Determine the base (scalar) type of the result.
+        base_type = self._get_result_scalar_type(argtypes)
+        if (isinstance(base_type, DeferredType) or
+                all(isinstance(atype, ScalarType) for atype in argtypes)):
+            # Both operands are of scalar type.
+            return base_type
+
+        if all(isinstance(atype, ArrayType) for atype in argtypes):
+            # Both operands are of array type.
+            if len(argtypes[0].shape) != len(argtypes[1].shape):
+                raise InternalError(
+                    f"Binary operation '{self.debug_string()}' has operands "
+                    f"of different shape: '{self.children[0].debug_string()}' "
+                    f"has rank {len(argtypes[0].shape)} and "
+                    f"'{self.children[1].debug_string()}' has rank "
+                    f"{len(argtypes[1].shape)}")
+            # In general there is no way we can check that the extents of each
+            # dimension match so we have to assume that they do.
+        shape = (argtypes[0].shape if isinstance(argtypes[0], ArrayType) else
+                 argtypes[1].shape)
+        return ArrayType(base_type, shape=shape)
+
 
 # For automatic API documentation generation
 __all__ = ["Operation", "UnaryOperation", "BinaryOperation"]
diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py
index a617e5bb2..f6a97d05d 100644
--- a/src/psyclone/psyir/symbols/datatypes.py
+++ b/src/psyclone/psyir/symbols/datatypes.py
@@ -433,7 +433,7 @@ class ArrayType(DataType):
                     "When creating an array of structures, the type of "
                     "those structures must be supplied as a DataTypeSymbol "
                     "but got a StructureType instead.")
-            if not isinstance(datatype, UnknownType):
+            if not isinstance(datatype, (UnknownType, DeferredType)):
                 self._intrinsic = datatype.intrinsic
                 self._precision = datatype.precision
             else:
@@ -740,8 +740,15 @@ class StructureType(DataType):
         '''
         Creates a StructureType from the supplied list of properties.
 
-        :param components: the name, type and visibility of each component.
-        :type components: list of 4-tuples
+        :param components: the name, type, visibility (whether public or
+            private) and initial value (if any) of each component.
+        :type components: List[tuple[
+            str,
+            :py:class:`psyclone.psyir.symbols.DataType` |
+                 :py:class:`psyclone.psyir.symbols.DataTypeSymbol`,
+            :py:class:`psyclone.psyir.symbols.Symbol.Visibility`,
+            Optional[:py:class:`psyclone.psyir.symbols.DataNode`]
+            ]]
 
         :returns: the new type object.
         :rtype: :py:class:`psyclone.psyir.symbols.StructureType`


Test patch:
diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
index 0877ff484..d770cdc29 100644
--- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
+++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
@@ -586,8 +586,8 @@ def test_get_indexed_field_argument_refs():
         assert isinstance(ref.symbol.datatype.partial_datatype, ArrayType)
         assert len(ref.symbol.datatype.partial_datatype.shape) == 1
         # The reference in a built-in will have a data type hard coded
-        assert isinstance(ref.datatype.partial_datatype, ArrayType)
-        assert ref.datatype.partial_datatype.precision.name == "r_def"
+        assert isinstance(ref.datatype, ScalarType)
+        assert ref.datatype.precision.name == "r_def"
 
 
 def test_get_scalar_argument_references():
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py b/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py
index 1427e47fe..17bd2461e 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_select_case_test.py
@@ -559,10 +559,8 @@ end interface psyclone_internal_cmp_1''' in fortran_writer(container)
 
 def test_expression_case(fortran_reader, fortran_writer):
     '''Test that a select case statement comparing two expressions
-    is using the generic comparison interface.
-
-    # TODO #1799: The interface may not be needed for this case if
-    # we can successuly obtain the expression.datatype
+    does not use the generic comparison interface if the types can be
+    determined.
 
     '''
     code = '''
@@ -584,12 +582,10 @@ def test_expression_case(fortran_reader, fortran_writer):
     psyir = fortran_reader.psyir_from_source(code)
     output = fortran_writer(psyir)
 
-    # Check that the interface implementation has been inserted
-    has_cmp_interface(output)
-
-    # Check that the cannonicalised comparisons use the interface method
-    assert "if (psyclone_internal_cmp(a * a, b - c)) then" in output
-    assert "if (psyclone_internal_cmp(a * a, c - b)) then" in output
+    # Check that the cannonicalised comparisons do not use the interface method
+    assert "if (a * a == b - c) then" in output
+    assert "if (a * a == c - b) then" in output
+    assert "interface psyclone_internal_cmp" not in output
 
 
 def test_unknown_types_case(fortran_reader, fortran_writer):
diff --git a/src/psyclone/tests/psyir/nodes/array_reference_test.py b/src/psyclone/tests/psyir/nodes/array_reference_test.py
index 8584e3637..70bada9f8 100644
--- a/src/psyclone/tests/psyir/nodes/array_reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_reference_test.py
@@ -500,7 +500,7 @@ def test_array_same_array():
     assert array.is_same_array(bare_array) is True
 
 
-def test_array_datatype(fortran_writer):
+def test_array_datatype():
     '''Test the datatype() method for an ArrayReference.'''
     test_sym = DataSymbol("test", ArrayType(REAL_TYPE, [10]))
     one = Literal("1", INTEGER_TYPE)
@@ -521,8 +521,8 @@ def test_array_datatype(fortran_writer):
     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)
+    # The easiest way to check the expression is to use debug_string()
+    code = upper.debug_string()
     assert code == "(4 - 2) / 1 + 1"
     # Reference to a single element of an array of structures.
     stype = DataTypeSymbol("grid_type", DeferredType())
@@ -535,7 +535,25 @@ def test_array_datatype(fortran_writer):
         "unknown",
         UnknownFortranType("real, dimension(5), pointer :: unknown"))
     aref = ArrayReference.create(unknown_sym, [two.copy()])
-    assert isinstance(aref.datatype, UnknownFortranType)
+    assert isinstance(aref.datatype, DeferredType)
+    # Reference to a single element of an array of UnknownType but with partial
+    # type information.
+    not_quite_unknown_sym = DataSymbol(
+        "unknown",
+        UnknownFortranType("real, dimension(5), pointer :: unknown",
+                           partial_datatype=ArrayType(REAL_SINGLE_TYPE, [5])))
+    bref = ArrayReference.create(not_quite_unknown_sym, [two.copy()])
+    assert bref.datatype == REAL_SINGLE_TYPE
+    # A sub-array of UnknownFortranType.
+    aref3 = ArrayReference.create(unknown_sym, [Range.create(two.copy(),
+                                                             four.copy())])
+    # We know the result is an ArrayType
+    assert isinstance(aref3.datatype, ArrayType)
+    assert aref3.datatype.shape[0].lower == one
+    upper = aref3.datatype.shape[0].upper
+    assert isinstance(upper, BinaryOperation)
+    # But we don't know the type of the array elements.
+    assert isinstance(aref3.datatype.intrinsic, DeferredType)
 
 
 def test_array_create_colon(fortran_writer):
diff --git a/src/psyclone/tests/psyir/nodes/datanode_test.py b/src/psyclone/tests/psyir/nodes/datanode_test.py
new file mode 100644
index 000000000..c4cf98e5c
--- /dev/null
+++ b/src/psyclone/tests/psyir/nodes/datanode_test.py
@@ -0,0 +1,50 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2023, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Author: A. R. Porter, STFC Daresbury Lab
+# -----------------------------------------------------------------------------
+
+'''Performs pytest tests on the PSyIR DataNode.
+
+'''
+from psyclone.psyir.nodes import DataNode
+from psyclone.psyir.symbols import DeferredType
+
+
+def test_datanode_datatype():
+    '''
+    Test that the base implementation of datatype just returns DeferredType.
+
+    '''
+    dnode = DataNode()
+    assert isinstance(dnode.datatype, DeferredType)
diff --git a/src/psyclone/tests/psyir/nodes/operation_test.py b/src/psyclone/tests/psyir/nodes/operation_test.py
index 9ecbe9eaa..de8aa1c19 100644
--- a/src/psyclone/tests/psyir/nodes/operation_test.py
+++ b/src/psyclone/tests/psyir/nodes/operation_test.py
@@ -40,16 +40,20 @@
 sub-classes.
 
 '''
+
+from enum import Enum
 import pytest
 
-from psyclone.psyir.nodes import UnaryOperation, BinaryOperation, \
-    Literal, Reference, Return
-from psyclone.psyir.symbols import DataSymbol, INTEGER_SINGLE_TYPE, \
-    REAL_SINGLE_TYPE
-from psyclone.errors import GenerationError
+from psyclone.errors import GenerationError, InternalError
 from psyclone.psyir.backend.fortran import FortranWriter
+from psyclone.psyir.nodes import (
+    ArrayReference, BinaryOperation, colored, IntrinsicCall,
+    Literal, Range, Reference, Return, StructureReference, UnaryOperation)
+from psyclone.psyir.symbols import (
+    ArrayType, BOOLEAN_TYPE, DataSymbol, DeferredType, INTEGER_SINGLE_TYPE,
+    REAL_DOUBLE_TYPE, REAL_SINGLE_TYPE, ScalarType, Symbol, StructureType,
+    UnknownFortranType)
 from psyclone.tests.utilities import check_links
-from psyclone.psyir.nodes import colored
 
 
 # Test BinaryOperation class
@@ -170,6 +174,338 @@ def test_binaryoperation_children_validation():
             "format is: 'DataNode, DataNode'.") in str(excinfo.value)
 
 
+def test_binaryop_scalar_datatype():
+    '''Test the datatype property of BinaryOperation for scalar arguments.'''
+    ref1 = Reference(DataSymbol("tmp1", REAL_SINGLE_TYPE))
+    ref2 = Reference(DataSymbol("tmp2", REAL_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.ADD
+    binaryoperation = BinaryOperation.create(oper, ref1, ref2)
+    assert binaryoperation.datatype == REAL_SINGLE_TYPE
+    iref1 = Reference(DataSymbol("itmp1", INTEGER_SINGLE_TYPE))
+    binop2 = BinaryOperation.create(oper, ref1.copy(), iref1)
+    assert binop2.datatype == REAL_SINGLE_TYPE
+    iref2 = Reference(DataSymbol("itmp2", INTEGER_SINGLE_TYPE))
+    binop3 = BinaryOperation.create(oper, iref1.copy(), iref2)
+    assert binop3.datatype == INTEGER_SINGLE_TYPE
+    # When any one of the arguments is of UnknownType then we know
+    # nothing about the type of the result.
+    uref1 = Reference(
+        DataSymbol("trouble",
+                   UnknownFortranType("real, volatile :: trouble")))
+    binop4 = BinaryOperation.create(oper, iref1.copy(), uref1)
+    # TODO #2419 - DeferredType should probably be UnsupportedType really.
+    assert isinstance(binop4.datatype, DeferredType)
+    binop5 = BinaryOperation.create(BinaryOperation.Operator.EQ,
+                                    iref1.copy(), iref2.copy())
+    assert binop5.datatype == BOOLEAN_TYPE
+    # Non-numeric type as operand of numerical operation.
+    binop6 = BinaryOperation.create(oper, iref1.copy(),
+                                    Reference(DataSymbol("switch",
+                                                         BOOLEAN_TYPE)))
+    with pytest.raises(TypeError) as err:
+        _ = binop6.datatype
+    assert ("Invalid argument of type 'Intrinsic.BOOLEAN' to numerical "
+            "operation 'Operator.ADD' in 'itmp1 + switch'" in str(err.value))
+
+
+def test_binaryop_get_result_precision(monkeypatch):
+    '''Test the _get_result_precision method of BinaryOperation gives the
+    correct precision.'''
+    ref1 = Reference(DataSymbol("tmp1", REAL_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.ADD
+    # Two arguments of different precision (SINGLE and DOUBLE)
+    ref2 = Reference(DataSymbol("dtmp1", REAL_DOUBLE_TYPE))
+    binop1 = BinaryOperation.create(oper, ref1, ref2)
+    bprecn1 = binop1._get_result_precision([ref1.datatype.precision,
+                                            ref2.datatype.precision])
+    assert bprecn1 == ScalarType.Precision.DOUBLE
+    # Two arguments with different (integer) precision
+    ref3 = Reference(DataSymbol("tmp3",
+                                ScalarType(ScalarType.Intrinsic.REAL, 4)))
+    dref3 = Reference(DataSymbol("dtmp3",
+                                 ScalarType(ScalarType.Intrinsic.REAL, 8)))
+    binop3 = BinaryOperation.create(oper, ref3, dref3)
+    bprecn3 = binop3._get_result_precision([ref3.datatype.precision,
+                                            dref3.datatype.precision])
+    assert bprecn3 == 8
+    # A mixture of precisions
+    binop4 = BinaryOperation.create(oper, ref1.copy(), ref3.copy())
+    bprecn4 = binop4._get_result_precision([ref1.datatype.precision,
+                                            ref3.datatype.precision])
+    assert bprecn4 == ScalarType.Precision.UNDEFINED
+    # One argument of undefined precision
+    ref5 = Reference(DataSymbol("tmp5",
+                                ScalarType(ScalarType.Intrinsic.REAL,
+                                           ScalarType.Precision.UNDEFINED)))
+    binop5 = BinaryOperation.create(oper, ref5, ref1.copy())
+    bprecn5 = binop5._get_result_precision([ref5.datatype.precision,
+                                            ref1.datatype.precision])
+    assert bprecn5 == ScalarType.Precision.UNDEFINED
+
+    # Exercise the check that we aren't missing a supported precision. We have
+    # to monkeypatch the enum containing the supported precisions for this.
+    class FakePrecision(Enum):
+        '''Fake version of Precision enum for testing.'''
+        SINGLE = 1
+        DOUBLE = 2
+        UNDEFINED = 3
+        OTHER = 4
+
+    monkeypatch.setattr(ScalarType, "Precision", FakePrecision)
+    monkeypatch.setattr(ref1.datatype, "_precision",
+                        ScalarType.Precision.SINGLE)
+    # pylint: disable=no-member
+    monkeypatch.setattr(ref5.datatype, "_precision",
+                        ScalarType.Precision.OTHER)
+    with pytest.raises(InternalError) as err:
+        _ = binop5._get_result_precision([ref5.datatype.precision,
+                                          ref1.datatype.precision])
+    assert ("got unsupported Precision value(s) 'FakePrecision.OTHER' and "
+            "'FakePrecision.SINGLE' for operands 'tmp5' and 'tmp1'"
+            in str(err.value))
+
+
+def test_binaryop_array_datatype():
+    '''Test the datatype property of BinaryOperation for array arguments.'''
+    arrtype = ArrayType(REAL_SINGLE_TYPE, [10])
+    iarrtype = ArrayType(INTEGER_SINGLE_TYPE, [5])
+    sym1 = DataSymbol("tmp1", arrtype)
+    sym2 = DataSymbol("tmp2", arrtype)
+    ref1 = Reference(sym1)
+    ref2 = Reference(sym2)
+    oper = BinaryOperation.Operator.ADD
+    # Addition of two arrays.
+    binaryoperation = BinaryOperation.create(oper, ref1, ref2)
+    dtype = binaryoperation.datatype
+    assert isinstance(dtype, ArrayType)
+    assert dtype == arrtype
+    # Add one element of an array to all elements of another array.
+    aref1 = ArrayReference.create(sym1, [Literal("2", INTEGER_SINGLE_TYPE)])
+    binop2 = BinaryOperation.create(oper, ref1.copy(), aref1)
+    dtype2 = binop2.datatype
+    assert isinstance(dtype2, ArrayType)
+    assert dtype2 == arrtype
+    # Add two elements of an array together.
+    binop3 = BinaryOperation.create(oper, aref1.copy(), aref1.copy())
+    dtype3 = binop3.datatype
+    assert dtype3 == REAL_SINGLE_TYPE
+    # Add a scalar integer to all elements of a real array.
+    binop4 = BinaryOperation.create(oper, ref1.copy(),
+                                    Literal("3", INTEGER_SINGLE_TYPE))
+    assert binop4.datatype == arrtype
+    # Add a real scalar to all elements of an integer array.
+    ref4 = Reference(DataSymbol("tmp4", iarrtype))
+    ref5 = Reference(DataSymbol("tmp5", REAL_SINGLE_TYPE))
+    binop5 = BinaryOperation.create(oper, ref4, ref5)
+    dtype5 = binop5.datatype
+    assert isinstance(dtype5, ArrayType)
+    assert len(dtype5.shape) == 1
+    assert dtype5.shape[0].lower.value == "1"
+    assert dtype5.shape[0].upper.value == "5"
+    assert dtype5.intrinsic == ScalarType.Intrinsic.REAL
+    # Non-conforming shapes.
+    arr2dtype = ArrayType(REAL_SINGLE_TYPE, [10, 5])
+    ref6 = Reference(DataSymbol("tmp2d", arr2dtype))
+    binop6 = BinaryOperation.create(oper, ref1.copy(), ref6)
+    with pytest.raises(InternalError) as err:
+        _ = binop6.datatype
+    assert ("Binary operation 'tmp1 + tmp2d' has operands of different shape: "
+            "'tmp1' has rank 1 and 'tmp2d' has rank 2" in str(err.value))
+
+
+def test_binaryop_array_section_datatype():
+    '''Test the datatype property of BinaryOperation when the operands are
+    array sections.'''
+    arrtype = ArrayType(REAL_SINGLE_TYPE, [10])
+    iarrtype = ArrayType(INTEGER_SINGLE_TYPE, [10, 5])
+    ref1 = ArrayReference.create(
+        DataSymbol("tmp1", arrtype),
+        [Range.create(Literal("2", INTEGER_SINGLE_TYPE),
+                      Literal("3", INTEGER_SINGLE_TYPE))])
+    ref2 = ArrayReference.create(
+        DataSymbol("tmp2", arrtype),
+        [Range.create(Literal("9", INTEGER_SINGLE_TYPE),
+                      Literal("10", INTEGER_SINGLE_TYPE))])
+    oper = BinaryOperation.Operator.ADD
+    # Addition of two array sections.
+    binaryoperation = BinaryOperation.create(oper, ref1, ref2)
+    dtype1 = binaryoperation.datatype
+    assert len(dtype1.shape) == 1
+    assert dtype1.shape[0].lower.value == "1"
+    assert dtype1.shape[0].upper.debug_string() == "(3 - 2) / 1 + 1"
+    # Repeat but for a non-contiguous 1D section of a rank 2, integer array.
+    ref3 = ArrayReference.create(
+        DataSymbol("tmp3", iarrtype),
+        [Literal("2", INTEGER_SINGLE_TYPE),
+         Range.create(Literal("4", INTEGER_SINGLE_TYPE),
+                      Literal("5", INTEGER_SINGLE_TYPE))])
+    binop3 = BinaryOperation.create(oper, ref1.copy(), ref3.copy())
+    dtype3 = binop3.datatype
+    assert dtype3.intrinsic == REAL_SINGLE_TYPE.intrinsic
+    assert len(dtype3.shape) == 1
+    assert dtype3.shape[0].lower.value == "1"
+    assert dtype3.shape[0].upper.debug_string() == "(3 - 2) / 1 + 1"
+
+
+def test_binaryop_datatype_recursion():
+    '''Test the datatype property of BinaryOperation when the operands are
+    themselves BinaryOperations.'''
+    iarrtype = ArrayType(INTEGER_SINGLE_TYPE, [10, 5])
+    ref1 = ArrayReference.create(
+        DataSymbol("tmp1", iarrtype),
+        [Range.create(Literal("2", INTEGER_SINGLE_TYPE),
+                      Literal("3", INTEGER_SINGLE_TYPE)),
+         Literal("3", INTEGER_SINGLE_TYPE)])
+    ref2 = ArrayReference.create(
+        DataSymbol("tmp2", iarrtype),
+        [Literal("2", INTEGER_SINGLE_TYPE),
+         Range.create(Literal("4", INTEGER_SINGLE_TYPE),
+                      Literal("5", INTEGER_SINGLE_TYPE))])
+    ref3 = Reference(DataSymbol("tmp3", REAL_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.SUB
+    binop1 = BinaryOperation.create(oper, ref2, ref3)
+    # Create tmp1(2:3) - (tmp2(4:5) - tmp3)
+    binop2 = BinaryOperation.create(oper, ref1, binop1)
+    dtype1 = binop2.datatype
+    assert isinstance(dtype1, ArrayType)
+    # Since tmp3 is real, the result should be real.
+    assert dtype1.intrinsic == REAL_SINGLE_TYPE.intrinsic
+    assert len(dtype1.shape) == 1
+    assert dtype1.shape[0].lower.value == "1"
+    assert dtype1.shape[0].upper.debug_string() == "(3 - 2) / 1 + 1"
+
+
+def test_binaryop_structure_datatype():
+    '''
+    Test the BinaryOperation datatype works when one or both arguments involve
+    structure types.
+
+    '''
+    arrtype = ArrayType(REAL_SINGLE_TYPE, [10, 5])
+    stype = StructureType.create([
+        ("nx", INTEGER_SINGLE_TYPE, Symbol.Visibility.PUBLIC, None),
+        ("data", arrtype, Symbol.Visibility.PUBLIC, None)])
+    sym1 = DataSymbol("field", stype)
+    ref1 = StructureReference.create(sym1, ["nx"])
+    oper = BinaryOperation.Operator.SUB
+    # field%nx - 2.0
+    binop1 = BinaryOperation.create(oper,
+                                    ref1, Literal("2.0", REAL_SINGLE_TYPE))
+    dtype1 = binop1.datatype
+    assert dtype1 == REAL_SINGLE_TYPE
+    ref2 = StructureReference.create(sym1, ["data"])
+    # field%nx - field%data
+    binop2 = BinaryOperation.create(oper, ref1.copy(), ref2)
+    dtype2 = binop2.datatype
+    assert isinstance(dtype2, ArrayType)
+    assert len(dtype2.shape) == 2
+    assert dtype2.shape == arrtype.shape
+    assert dtype2.intrinsic == REAL_SINGLE_TYPE.intrinsic
+
+
+def test_binaryop_deferred_datatype():
+    '''
+    Test that the BinaryOperation datatype always returns DeferredType if
+    either (or both) operand(s) is of DeferredType.
+
+    '''
+    wind = Reference(DataSymbol("wind", DeferredType()))
+    sea = Reference(DataSymbol("sea", INTEGER_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.ADD
+    binop1 = BinaryOperation.create(oper, wind, sea)
+    assert isinstance(binop1.datatype, DeferredType)
+    binop2 = BinaryOperation.create(oper, sea.copy(), wind.copy())
+    assert isinstance(binop2.datatype, DeferredType)
+    binop3 = BinaryOperation.create(oper, wind.copy(), wind.copy())
+    assert isinstance(binop3.datatype, DeferredType)
+    # An Array of deferred type.
+    arrtype = ArrayType(DeferredType(), [10])
+    air = Reference(DataSymbol("air", arrtype))
+    binop4 = BinaryOperation.create(oper, air, sea.copy())
+    assert isinstance(binop4.datatype, DeferredType)
+
+
+def test_binaryop_partial_datatype():
+    '''
+    Test the BinaryOperation datatype works when one or both arguments only
+    have partial type information.
+
+    '''
+    iarrtype = ArrayType(INTEGER_SINGLE_TYPE, [10, 5])
+    utype = UnknownFortranType("integer, dimension(10,5), pointer :: ref1",
+                               partial_datatype=iarrtype)
+    ref1 = Reference(DataSymbol("ref1", utype))
+    ref2 = Reference(DataSymbol("ref2", REAL_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.MUL
+    binop1 = BinaryOperation.create(oper, ref1, ref2)
+    dtype1 = binop1.datatype
+    assert isinstance(dtype1, ArrayType)
+    assert dtype1.intrinsic == REAL_SINGLE_TYPE.intrinsic
+    # Create a second Symbol of UnknownFortranType.
+    arrtype3 = ArrayType(REAL_SINGLE_TYPE, [10, 10])
+    utype3 = UnknownFortranType("real, dimension(10,10), pointer :: ref3",
+                                partial_datatype=arrtype3)
+    # We are unable to determine the type of a Reference to a
+    # subsection of an array of UnknownType (since that would require
+    # updating the original UnknownType with a new shape).
+    ref3 = ArrayReference.create(
+        DataSymbol("ref3", utype3),
+        [":", Range.create(Literal("5", INTEGER_SINGLE_TYPE),
+                           Literal("10", INTEGER_SINGLE_TYPE))])
+    # Create ref1(:,:) * ref3(:,5:10)
+    binop3 = BinaryOperation.create(oper, ref1.copy(), ref3)
+    dtype3 = binop3.datatype
+    assert isinstance(dtype3, DeferredType)
+    # However, if a subsection is not involved then we are OK.
+    arrtype4 = ArrayType(REAL_SINGLE_TYPE, [10, 5])
+    utype4 = UnknownFortranType("real, dimension(10,5), pointer :: ref3",
+                                partial_datatype=arrtype4)
+    ref4 = Reference(DataSymbol("ref4", utype4))
+    binop4 = BinaryOperation.create(oper, ref1.copy(), ref4)
+    dtype4 = binop4.datatype
+    assert len(dtype4.shape) == 2
+    assert dtype4.shape[0].lower.value == "1"
+    assert dtype4.shape[0].upper.value == "10"
+    assert dtype4.shape[1].lower.value == "1"
+    assert dtype4.shape[1].upper.value == "5"
+    assert dtype4.intrinsic == REAL_SINGLE_TYPE.intrinsic
+    # A reference to an array of unknown type but with partial type info.
+    utype5 = UnknownFortranType("real, pointer :: ref5",
+                                partial_datatype=REAL_SINGLE_TYPE)
+    arrtype5 = ArrayType(utype5, [8])
+    ref5 = Reference(DataSymbol("ref5", arrtype5))
+    binop5 = BinaryOperation.create(oper, ref2.copy(), ref5)
+    dtype5 = binop5.datatype
+    assert isinstance(dtype5, ArrayType)
+    assert dtype5.intrinsic == REAL_SINGLE_TYPE.intrinsic
+    assert len(dtype5.shape) == 1
+    # Reference to an array of unknown type without partialy type information.
+    utype6 = UnknownFortranType("real, dimension(10,5), pointer :: ref6")
+    arrtype6 = ArrayType(utype6, [10, 5])
+    ref6 = Reference(DataSymbol("ref6", arrtype6))
+    binop6 = BinaryOperation.create(oper, ref4.copy(), ref6)
+    dtype6 = binop6.datatype
+    assert isinstance(dtype6, DeferredType)
+
+
+def test_binaryoperation_intrinsic_fn_datatype():
+    '''
+    Check that we can get the datatype of an operation involving the result
+    of an intrinsic function.
+
+    TODO #1799 - this just returns DeferredType at the minute and needs
+    implementing.
+
+    '''
+    arrtype = ArrayType(REAL_SINGLE_TYPE, [10, 5])
+    aref = Reference(DataSymbol("array", arrtype))
+    arg1 = IntrinsicCall.create(IntrinsicCall.Intrinsic.MAXVAL, [aref])
+    arg2 = Reference(DataSymbol("scalar", INTEGER_SINGLE_TYPE))
+    oper = BinaryOperation.Operator.ADD
+    binop = BinaryOperation.create(oper, arg1, arg2)
+    assert isinstance(binop.datatype, DeferredType)
+
+
 # Test UnaryOperation class
 def test_unaryoperation_initialization():
     ''' Check the initialization method of the UnaryOperation class works
@@ -268,6 +604,34 @@ def test_unaryoperation_children_validation():
             "format is: 'DataNode'.") in str(excinfo.value)
 
 
+def test_unaryop_datatype():
+    '''
+    Test the datatype property of UnaryOperation.
+    '''
+    # Numerical, scalar operation
+    oper = UnaryOperation.Operator.MINUS
+    uop = UnaryOperation.create(oper, Literal("1", INTEGER_SINGLE_TYPE))
+    assert uop.datatype == INTEGER_SINGLE_TYPE
+    # Numerical, array operation
+    ntype = ArrayType(REAL_DOUBLE_TYPE, [20])
+    nsym = DataSymbol("var", ntype)
+    uop1 = UnaryOperation.create(oper, Reference(nsym))
+    dtype = uop1.datatype
+    assert dtype == ntype
+    # Logical operation
+    oper = UnaryOperation.Operator.NOT
+    uop2 = UnaryOperation.create(oper, Literal("true", BOOLEAN_TYPE))
+    assert uop2.datatype == BOOLEAN_TYPE
+    # With logical array argument
+    atype = ArrayType(BOOLEAN_TYPE, [10, 10])
+    asym = DataSymbol("mask", atype)
+    uop3 = UnaryOperation.create(oper, Reference(asym))
+    dtype = uop3.datatype
+    assert isinstance(dtype, ArrayType)
+    assert dtype == atype
+    assert dtype.intrinsic == ScalarType.Intrinsic.BOOLEAN
+
+
 def test_operations_can_be_copied():
     ''' Test that an operation can be copied. '''