Repository: stfc/PSyclone

Issue:
Apply lower_to_language in all LFRic built-ins
The `gen_code` method in some of the LFRic built-ins has already been replaced with `lower_to_langauge` method instead. This issue replaces the methods in all the remaining LFRic built-ins.

Discussion:

Patch:
diff --git a/changelog b/changelog
index 9ea563543..da94a09b0 100644
--- a/changelog
+++ b/changelog
@@ -72,6 +72,8 @@
 	27) PR #2364 towards #1941. Moves map of LFRic precisions to
 	config. file.
 
+	28) PR #2376 for #1525. Add lowering methods to all LFRic Built-ins.
+
 release 2.4.0 29th of September 2023
 
 	1) PR #1758 for #1741. Splits the PSyData read functionality into a
diff --git a/doc/user_guide/dynamo0p3.rst b/doc/user_guide/dynamo0p3.rst
index e2af07334..fdc0bb10c 100644
--- a/doc/user_guide/dynamo0p3.rst
+++ b/doc/user_guide/dynamo0p3.rst
@@ -3277,11 +3277,11 @@ int_X
 **int_X** (**ifield2**, *field1*)
 
 Converts ``real``-valued field elements to ``integer``-valued field
-elements, e.g. in Fortran this would be: ``Y = int(X, i_def)``.
+elements, e.g. in Fortran this would be: ``Y = INT(X, kind=i_def)``.
 Here ``Y`` is an ``integer``-valued field and ``X`` is the
 ``real``-valued field being converted::
 
-  ifield2(:) = INT(field1(:), i_def)
+  ifield2(:) = INT(field1(:), kind=i_def)
 
 where **ifield2** is an ``integer_field_type`` of ``i_def`` precision
 and a ``real``-valued field *field1* can be of any :ref:`supported
@@ -3579,12 +3579,12 @@ real_X
 
 **real_X** (**field2**, *ifield1*)
 
-Converts ``integer``-valued field elements to ``real``-valued
-field elements, e.g. in Fortran this would be ``Y = real(X, r_def)``.
+Converts ``integer``-valued field elements to ``real``-valued field
+elements, e.g. in Fortran this would be ``Y = REAL(X, kind=r_def)``.
 Here ``Y`` is a ``real``-valued field and ``X`` is the
 ``integer``-valued field being converted::
 
-  field2(:) = REAL(ifield1(:), r_<prec>)
+  field2(:) = REAL(ifield1(:), kind=r_<prec>)
 
 where *ifield1* is an ``integer_field_type`` of ``i_def`` precision.
 The ``real``-valued **field1** can be of any :ref:`supported
diff --git a/src/psyclone/domain/lfric/lfric_builtins.py b/src/psyclone/domain/lfric/lfric_builtins.py
index 6878b8f71..0ff83a349 100644
--- a/src/psyclone/domain/lfric/lfric_builtins.py
+++ b/src/psyclone/domain/lfric/lfric_builtins.py
@@ -51,7 +51,7 @@ from psyclone.domain.lfric.kernel import (
     LFRicKernelMetadata, FieldArgMetadata, ScalarArgMetadata,
     FieldVectorArgMetadata)
 from psyclone.errors import InternalError
-from psyclone.f2pygen import AssignGen, PSyIRGen
+from psyclone.f2pygen import PSyIRGen
 from psyclone.parse.utils import ParseError
 from psyclone.psyGen import BuiltIn
 from psyclone.psyir.nodes import (ArrayReference, Assignment, BinaryOperation,
@@ -361,14 +361,6 @@ class LFRicBuiltIn(BuiltIn, metaclass=abc.ABCMeta):
                 f" different data types. However, found different data types "
                 f"{data_types_str} for field arguments to '{self.name}'.")
 
-    def array_ref(self, fld_name):
-        '''
-        :returns: the array reference for a variable with the supplied name.
-        :rtype: str
-
-        '''
-        return f"{fld_name}({self._idx_name})"
-
     @property
     def undf_name(self):
         '''
@@ -508,61 +500,6 @@ class LFRicBuiltIn(BuiltIn, metaclass=abc.ABCMeta):
                 if arg.is_scalar]
 
 
-class LFRicXKern(LFRicBuiltIn, metaclass=abc.ABCMeta):
-    '''Abstract class providing functionaliy to convert a field of
-    one type to a field of another type. If [Datatype] (stored in
-    _field_type) is the particular datatype to convert to and
-    [Precision] is the precision of this datatype then the result is
-    `Y = [Datatype](X, [Precision])`. Here `Y` is a field of type
-    [Datatype] and `X` is a field with a different type. The correct
-    [Precision] is picked up from the associated argument.
-
-    '''
-    _field_type = None
-
-    def gen_code(self, parent):
-        '''Generates LFRic API specific PSy code for a call to the
-        [datatype]_X built-in.
-
-        :param parent: Node in f2pygen tree to which to add call.
-        :type parent: :py:class:`psyclone.f2pygen.BaseGen`
-
-        :raises InternalError: if the _field_type variable is not set \
-            correctly.
-
-        '''
-        # Subclass must set _field_type
-        if not self._field_type:
-            raise InternalError(
-                "Subclasses of LFRicXKern must set the _field_type variable "
-                "to the output datatype.")
-        table = self.scope.symbol_table
-        # Convert all the elements of a field of one type to the
-        # corresponding elements of a field of another type using
-        # the PSyclone configuration for the correct 'kind'.
-        suffixes = LFRicConstants().ARG_TYPE_SUFFIX_MAPPING
-        args = self._arguments.args
-        sym2 = table.lookup_with_tag(
-            f"{args[0].name}:{suffixes[args[0].argument_type]}")
-        field2 = self.array_ref(sym2.name)
-        sym1 = table.lookup_with_tag(
-            f"{args[1].name}:{suffixes[args[1].argument_type]}")
-        field1 = self.array_ref(sym1.name)
-        precision = self._arguments.args[0].precision
-        rhs_expr = f"{self._field_type}({field1}, {precision})"
-        parent.add(AssignGen(parent, lhs=field2, rhs=rhs_expr))
-        # Import the precision variable if it is not already imported
-        const = LFRicConstants()
-        const_mod = const.UTILITIES_MOD_MAP["constants"]["module"]
-        # Import node type here to avoid circular dependencies
-        # pylint: disable=import-outside-toplevel
-        from psyclone.dynamo0p3 import DynInvokeSchedule
-        schedule = self.ancestor(DynInvokeSchedule)
-        psy = schedule.invoke.invokes.psy
-        precision_uses = psy.infrastructure_modules[const_mod]
-        precision_uses.add(precision)
-
-
 # ******************************************************************* #
 # ************** Built-ins for real-valued fields ******************* #
 # ******************************************************************* #
@@ -2379,31 +2316,29 @@ class LFRicXInnerproductYKern(LFRicBuiltIn):
             FieldArgMetadata(gh_datatype, "gh_read", "any_space_1"),
             FieldArgMetadata(gh_datatype, "gh_read", "any_space_1")])
 
-    def gen_code(self, parent):
+    def lower_to_language_level(self):
         '''
-        Generates LFRic API specific PSy code for a call to the
-        X_innerproduct_Y Built-in.
+        Lowers this LFRic-specific built-in kernel to language-level PSyIR.
+        This BuiltIn node is replaced by an Assignment node.
 
-        :param parent: Node in f2pygen tree to which to add call.
-        :type parent: :py:class:`psyclone.f2pygen.BaseGen`
+        :returns: the lowered version of this node.
+        :rtype: :py:class:`psyclone.psyir.node.Node`
 
         '''
-        table = self.scope.symbol_table
-        # We sum the DoF-wise product of the supplied real-valued fields.
-        # The real scalar variable holding the sum is initialised to zero
-        # in the PSy layer.
-        innprod_name = self._reduction_ref(self._arguments.args[0].name)
-        suffixes = LFRicConstants().ARG_TYPE_SUFFIX_MAPPING
-
-        args = self._arguments.args
-        sym1 = table.lookup_with_tag(
-            f"{args[1].name}:{suffixes[args[1].argument_type]}")
-        field1 = self.array_ref(sym1.name)
-        sym2 = table.lookup_with_tag(
-            f"{args[2].name}:{suffixes[args[2].argument_type]}")
-        field2 = self.array_ref(sym2.name)
-        rhs_expr = f"{innprod_name} + {field1}*{field2}"
-        parent.add(AssignGen(parent, lhs=innprod_name, rhs=rhs_expr))
+        # Get indexed references for the field (proxy) argument.
+        arg_refs = self.get_indexed_field_argument_references()
+        # Get a reference for the kernel scalar reduction argument.
+        lhs = self._reduction_reference()
+        # Create the PSyIR for the kernel:
+        #      asum = asum + proxy0%data(df) * proxy1%data(df)
+        mult_op = BinaryOperation.create(BinaryOperation.Operator.MUL,
+                                         arg_refs[0], arg_refs[1])
+        rhs = BinaryOperation.create(BinaryOperation.Operator.ADD,
+                                     lhs.copy(), mult_op)
+        assign = Assignment.create(lhs, rhs)
+        # Finally, replace this kernel node with the Assignment
+        self.replace_with(assign)
+        return assign
 
 
 class LFRicXInnerproductXKern(LFRicBuiltIn):
@@ -2427,27 +2362,29 @@ class LFRicXInnerproductXKern(LFRicBuiltIn):
             ScalarArgMetadata(gh_datatype, "gh_sum"),
             FieldArgMetadata(gh_datatype, "gh_read", "any_space_1")])
 
-    def gen_code(self, parent):
+    def lower_to_language_level(self):
         '''
-        Generates LFRic API specific PSy code for a call to the
-        X_innerproduct_X Built-in.
+        Lowers this LFRic-specific built-in kernel to language-level PSyIR.
+        This BuiltIn node is replaced by an Assignment node.
 
-        :param parent: Node in f2pygen tree to which to add call.
-        :type parent: :py:class:`psyclone.f2pygen.BaseGen`
+        :returns: the lowered version of this node.
+        :rtype: :py:class:`psyclone.psyir.node.Node`
 
         '''
-        table = self.scope.symbol_table
-        suffixes = LFRicConstants().ARG_TYPE_SUFFIX_MAPPING
-        # We sum the DoF-wise product of the supplied real-valued fields.
-        # The real scalar variable holding the sum is initialised to zero
-        # in the PSy layer.
-        innprod_name = self._reduction_ref(self._arguments.args[0].name)
-        sym = table.lookup_with_tag(
-            f"{self._arguments.args[1].name}:"
-            f"{suffixes[self._arguments.args[1].argument_type]}")
-        field_name = self.array_ref(sym.name)
-        rhs_expr = f"{innprod_name} + {field_name}*{field_name}"
-        parent.add(AssignGen(parent, lhs=innprod_name, rhs=rhs_expr))
+        # Get indexed references for the field (proxy) argument.
+        arg_refs = self.get_indexed_field_argument_references()
+        # Get a reference for the kernel scalar reduction argument.
+        lhs = self._reduction_reference()
+        # Create the PSyIR for the kernel:
+        #      asum = asum + proxy0%data(df) * proxy0%data(df)
+        mult_op = BinaryOperation.create(BinaryOperation.Operator.MUL,
+                                         arg_refs[0].copy(), arg_refs[0])
+        rhs = BinaryOperation.create(BinaryOperation.Operator.ADD,
+                                     lhs.copy(), mult_op)
+        assign = Assignment.create(lhs, rhs)
+        # Finally, replace this kernel node with the Assignment
+        self.replace_with(assign)
+        return assign
 
 
 # ------------------------------------------------------------------- #
@@ -2479,26 +2416,27 @@ class LFRicSumXKern(LFRicBuiltIn):
         return (f"Built-in: {self._case_name} (sum {a_or_an(self._datatype)} "
                 f"{self._datatype}-valued field)")
 
-    def gen_code(self, parent):
+    def lower_to_language_level(self):
         '''
-        Generates LFRic API specific PSy code for a call to the
-        sum_X Built-in.
+        Lowers this LFRic-specific built-in kernel to language-level PSyIR.
+        This BuiltIn node is replaced by an Assignment node.
 
-        :param parent: Node in f2pygen tree to which to add call.
-        :type parent: :py:class:`psyclone.f2pygen.BaseGen`
+        :returns: the lowered version of this node.
+        :rtype: :py:class:`psyclone.psyir.node.Node`
 
         '''
-        table = self.scope.symbol_table
-        suffixes = LFRicConstants().ARG_TYPE_SUFFIX_MAPPING
-        # Sum all the elements of a real-valued field. The real scalar
-        # variable holding the sum is initialised to zero in the PSy layer.
-        sym = table.lookup_with_tag(
-            f"{self._arguments.args[1].name}:"
-            f"{suffixes[self._arguments.args[1].argument_type]}")
-        field_name = self.array_ref(sym.name)
-        sum_name = self._reduction_ref(self._arguments.args[0].name)
-        rhs_expr = f"{sum_name} + {field_name}"
-        parent.add(AssignGen(parent, lhs=sum_name, rhs=rhs_expr))
+        # Get indexed references for the field (proxy) argument.
+        arg_refs = self.get_indexed_field_argument_references()
+        # Get a reference for the kernel scalar reduction argument.
+        lhs = self._reduction_reference()
+        # Create the PSyIR for the kernel:
+        #      asum = asum + proxy0%data(df)
+        rhs = BinaryOperation.create(BinaryOperation.Operator.ADD,
+                                     lhs.copy(), arg_refs[0])
+        assign = Assignment.create(lhs, rhs)
+        # Finally, replace this kernel node with the Assignment
+        self.replace_with(assign)
+        return assign
 
 
 # ------------------------------------------------------------------- #
@@ -2759,17 +2697,15 @@ class LFRicIncMinAXKern(LFRicBuiltIn):
 # ------------------------------------------------------------------- #
 
 
-class LFRicIntXKern(LFRicXKern):
+class LFRicIntXKern(LFRicBuiltIn):
     ''' Converts real-valued field elements to integer-valued
-    field elements using the Fortran intrinsic `int` function,
-    `Y = int(X, r_def)`. Here `Y` is an int-valued field and `X`
-    is the real-valued field being converted.
+    field elements using the Fortran intrinsic `INT` function,
+    `Y = INT(X, kind=i_<prec>)`. Here `Y` is an integer-valued
+    field of precision `i_<prec>` and `X` is the real-valued
+    field being converted.
 
     '''
-    # '_datatype' is only required to avoid an exception in '__init__',
-    # it is not required by this class.
-    _datatype = "dummy"
-    _field_type = "int"
+    _datatype = "integer"
     _case_name = "int_X"
 
     @classmethod
@@ -2788,6 +2724,29 @@ class LFRicIntXKern(LFRicXKern):
         return (f"Built-in: {self._case_name} (convert a real-valued to "
                 f"an integer-valued field)")
 
+    def lower_to_language_level(self):
+        '''
+        Lowers this LFRic-specific built-in kernel to language-level PSyIR.
+        This BuiltIn node is replaced by an Assignment node.
+
+        :returns: the lowered version of this node.
+        :rtype: :py:class:`psyclone.psyir.node.Node`
+
+        '''
+        # Get indexed references for each of the field (proxy) arguments.
+        arg_refs = self.get_indexed_field_argument_references()
+
+        # Create the PSyIR for the kernel:
+        #      proxy0%data(df) = INT(proxy0%data, kind=i_<prec>)
+        i_precision = arg_refs[0].datatype.partial_datatype.precision
+        rhs = IntrinsicCall.create(
+            IntrinsicCall.Intrinsic.INT,
+            [arg_refs[1], ("kind", Reference(i_precision))])
+        assign = Assignment.create(arg_refs[0], rhs)
+        # Finally, replace this kernel node with the Assignment
+        self.replace_with(assign)
+        return assign
+
 
 # ******************************************************************* #
 # ************** Built-ins for integer-valued fields **************** #
@@ -3081,18 +3040,15 @@ class LFRicIntIncMinAXKern(LFRicIncMinAXKern):
 # ============== Converting integer to real field elements ========== #
 # ------------------------------------------------------------------- #
 
-
-class LFRicRealXKern(LFRicXKern):
+class LFRicRealXKern(LFRicBuiltIn):
     ''' Converts integer-valued field elements to real-valued
-    field elements using the Fortran intrinsic `real` function,
-    `Y = real(X, r_def)`. Here `Y` is a real-valued field and `X`
-    is the integer-valued field being converted.
+    field elements using the Fortran intrinsic `REAL` function,
+    `Y = REAL(X, kind=r_<prec>)`. Here `Y` is a real-valued
+    field of precision `r_<prec>` and `X` is the integer-valued
+    field being converted.
 
     '''
-    # '_datatype' is only required to avoid an exception in '__init__',
-    # it is not required by this class.
-    _datatype = "dummy"
-    _field_type = "real"
+    _datatype = "real"
     _case_name = "real_X"
 
     @classmethod
@@ -3111,6 +3067,29 @@ class LFRicRealXKern(LFRicXKern):
         return (f"Built-in: {self._case_name} (convert an integer-valued "
                 f"to a real-valued field)")
 
+    def lower_to_language_level(self):
+        '''
+        Lowers this LFRic-specific built-in kernel to language-level PSyIR.
+        This BuiltIn node is replaced by an Assignment node.
+
+        :returns: the lowered version of this node.
+        :rtype: :py:class:`psyclone.psyir.node.Node`
+
+        '''
+        # Get indexed references for each of the field (proxy) arguments.
+        arg_refs = self.get_indexed_field_argument_references()
+
+        # Create the PSyIR for the kernel:
+        #      proxy0%data(df) = REAL(proxy0%data, kind=r_<prec>)
+        r_precision = arg_refs[0].datatype.partial_datatype.precision
+        rhs = IntrinsicCall.create(
+            IntrinsicCall.Intrinsic.REAL,
+            [arg_refs[1], ("kind", Reference(r_precision))])
+        assign = Assignment.create(arg_refs[0], rhs)
+        # Finally, replace this kernel node with the Assignment
+        self.replace_with(assign)
+        return assign
+
 
 # The built-in operations that we support for this API. The meta-data
 # describing these kernels is in lfric_builtins_mod.f90. This dictionary
diff --git a/src/psyclone/domain/lfric/lfric_extract_driver_creator.py b/src/psyclone/domain/lfric/lfric_extract_driver_creator.py
index d3ac2edd0..8e27ca405 100644
--- a/src/psyclone/domain/lfric/lfric_extract_driver_creator.py
+++ b/src/psyclone/domain/lfric/lfric_extract_driver_creator.py
@@ -50,7 +50,7 @@ from psyclone.psyGen import InvokeSchedule, Kern
 from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.psyir.frontend.fortran import FortranReader
 from psyclone.psyir.nodes import (Assignment, Call, FileContainer,
-                                  IntrinsicCall, Literal, Node, Reference,
+                                  IntrinsicCall, Literal, Reference,
                                   Routine, StructureReference)
 from psyclone.psyir.symbols import (ArrayType, CHARACTER_TYPE,
                                     ContainerSymbol, DataSymbol,
@@ -736,18 +736,6 @@ class LFRicExtractDriverCreator:
         # We need to provide the prefix to the validation function:
         extract_trans.validate(nodes, options={"prefix": prefix})
 
-        # Avoid circular import
-        # pylint: disable=import-outside-toplevel
-        from psyclone.domain.lfric.lfric_builtins import LFRicBuiltIn
-        for node in nodes:
-            for builtin in node.walk(LFRicBuiltIn):
-                # If the lower_to_language function is not overwritten from
-                # the implementation in Node, the builtin is not yet supported:
-                if type(builtin).lower_to_language_level == \
-                        Node.lower_to_language_level:
-                    raise NotImplementedError(
-                        f"LFRic builtin '{builtin.name}' is not supported")
-
         module_name, local_name = region_name
         unit_name = self._make_valid_unit_name(f"{module_name}_{local_name}")
 
@@ -887,38 +875,43 @@ class LFRicExtractDriverCreator:
 
         :param nodes: a list of nodes.
         :type nodes: List[:py:class:`psyclone.psyir.nodes.Node`]
-        :param read_write_info: information about all input and output \
+        :param read_write_info: information about all input and output
             parameters.
         :type read_write_info: :py:class:`psyclone.psyir.tools.ReadWriteInfo`
-        :param str prefix: the prefix to use for each PSyData symbol, \
+        :param str prefix: the prefix to use for each PSyData symbol,
             e.g. 'extract' as prefix will create symbols `extract_psydata`.
-        :param str postfix: a postfix that is appended to an output variable \
-            to create the corresponding variable that stores the output \
-            value from the kernel data file. The caller must guarantee that \
-            no name clashes are created when adding the postfix to a variable \
-            and that the postfix is consistent between extract code and \
+        :param str postfix: a postfix that is appended to an output variable
+            to create the corresponding variable that stores the output
+            value from the kernel data file. The caller must guarantee that
+            no name clashes are created when adding the postfix to a variable
+            and that the postfix is consistent between extract code and
             driver code (see 'ExtractTrans.determine_postfix()').
-        :param Tuple[str,str] region_name: an optional name to \
-            use for this PSyData area, provided as a 2-tuple containing a \
-            location name followed by a local name. The pair of strings \
+        :param Tuple[str,str] region_name: an optional name to
+            use for this PSyData area, provided as a 2-tuple containing a
+            location name followed by a local name. The pair of strings
             should uniquely identify a region.
-        :param language_writer: a backend visitor to convert PSyIR \
-            representation to the selected language. It defaults to \
+        :param language_writer: a backend visitor to convert PSyIR
+            representation to the selected language. It defaults to
             the FortranWriter.
-        :type language_writer: \
+        :type language_writer:
             :py:class:`psyclone.psyir.backend.language_writer.LanguageWriter`
 
         :returns: the driver in the selected language.
         :rtype: str
 
+        :raises NotImplementedError: if the driver creation fails.
+
         '''
         try:
             file_container = self.create(nodes, read_write_info, prefix,
                                          postfix, region_name)
-        except NotImplementedError as err:
-            print(f"Cannot create driver for '{region_name[0]}-"
-                  f"{region_name[1]}' because:")
-            print(str(err))
+        # TODO #2120 (Handle failures in Kernel Extraction): Now that all
+        # built-ins are lowered, an alternative way of triggering a
+        # NotImplementedError is needed.
+        except NotImplementedError:
+            # print(f"Cannot create driver for '{region_name[0]}-"
+            #      f"{region_name[1]}' because:")
+            # print(str(err))
             return ""
 
         module_dependencies = self.collect_all_required_modules(file_container)
@@ -983,6 +976,8 @@ class LFRicExtractDriverCreator:
         if not code:
             # This indicates an error that was already printed,
             # so ignore it here.
+            # TODO #2120 (Handle failures in Kernel Extraction): revisit
+            # how this is handled in 'get_driver_as_string'.
             return
         module_name, local_name = region_name
         with open(f"driver-{module_name}-{local_name}.F90", "w",
diff --git a/src/psyclone/domain/lfric/lfric_kern.py b/src/psyclone/domain/lfric/lfric_kern.py
index 1749760b4..084c311c3 100644
--- a/src/psyclone/domain/lfric/lfric_kern.py
+++ b/src/psyclone/domain/lfric/lfric_kern.py
@@ -869,7 +869,7 @@ class LFRicKern(CodedKern):
                 f"API expects '{expected_datatype}'.")
         # 2: precision. An LFRic kernel is only permitted to have a precision
         #    specified by a recognised type parameter or a no. of bytes.
-        actual_precision = kern_code_arg.datatype.precision        
+        actual_precision = kern_code_arg.datatype.precision
         api_config = Config.get().api_conf("dynamo0.3")
         if isinstance(actual_precision, DataSymbol):
             # Convert precision into number of bytes to support
diff --git a/src/psyclone/domain/lfric/lfric_kern_call_factory.py b/src/psyclone/domain/lfric/lfric_kern_call_factory.py
index bf28704fb..4ee55bd8f 100644
--- a/src/psyclone/domain/lfric/lfric_kern_call_factory.py
+++ b/src/psyclone/domain/lfric/lfric_kern_call_factory.py
@@ -44,6 +44,7 @@
 # Imports
 from psyclone.domain.lfric import LFRicKern
 
+
 class LFRicKernCallFactory():
     ''' Create the necessary framework for an LFRic kernel call.
     This consists of a Loop over cells containing a call to the
diff --git a/src/psyclone/domain/lfric/lfric_symbol_table.py b/src/psyclone/domain/lfric/lfric_symbol_table.py
index eaeea2f90..3d4a9747f 100644
--- a/src/psyclone/domain/lfric/lfric_symbol_table.py
+++ b/src/psyclone/domain/lfric/lfric_symbol_table.py
@@ -80,7 +80,7 @@ class LFRicSymbolTable(SymbolTable):
             const = LFRicConstants()
             mod_name = const.UTILITIES_MOD_MAP["constants"]["module"]
             LFRicSymbolTable._constants_mod = ContainerSymbol(mod_name)
-            
+
             api_config = Config.get().api_conf("dynamo0.3")
             for precision in api_config.precision_map:
                 LFRicSymbolTable._precision_map[precision] = \
diff --git a/src/psyclone/dynamo0p3.py b/src/psyclone/dynamo0p3.py
index 1f3ea4338..064ebee88 100644
--- a/src/psyclone/dynamo0p3.py
+++ b/src/psyclone/dynamo0p3.py
@@ -3282,7 +3282,6 @@ class DynCellIterators(LFRicCollection):
                 self._first_var.ref_name() + "%get_nlayers()"))
 
 
-
 class DynLMAOperators(LFRicCollection):
     '''
     Handles all entities associated with Local-Matrix-Assembly Operators.
diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py
index 9ebb10076..ab78c2456 100644
--- a/src/psyclone/psyGen.py
+++ b/src/psyclone/psyGen.py
@@ -51,11 +51,13 @@ from psyclone.f2pygen import (AllocateGen, AssignGen, CallGen, CommentGen,
                               DeclGen, DeallocateGen, DoGen, UseGen)
 from psyclone.parse.algorithm import BuiltInCall
 from psyclone.psyir.backend.fortran import FortranWriter
-from psyclone.psyir.nodes import (Node, Schedule, Loop, Statement, Container,
-                                  Routine, Call, OMPDoDirective)
-from psyclone.psyir.symbols import (ArrayType, DataSymbol, RoutineSymbol,
-                                    Symbol, ContainerSymbol, ImportInterface,
-                                    ArgumentInterface, DeferredType)
+from psyclone.psyir.nodes import (ArrayReference, Call, Container, Literal,
+                                  Loop, Node, OMPDoDirective, Reference,
+                                  Routine, Schedule, Statement)
+from psyclone.psyir.symbols import (ArgumentInterface, ArrayType,
+                                    ContainerSymbol, DataSymbol, DeferredType,
+                                    ImportInterface, INTEGER_TYPE,
+                                    RoutineSymbol, Symbol)
 from psyclone.psyir.symbols.datatypes import UnknownFortranType
 
 # The types of 'intent' that an argument to a Fortran subroutine
@@ -1107,22 +1109,33 @@ class Kern(Statement):
 
     @property
     def is_reduction(self):
-        '''if this kernel/builtin contains a reduction variable then return
-        True, otherwise return False'''
+        '''
+        :returns: whether this kernel/built-in contains a reduction variable.
+        :rtype: bool
+
+        '''
         return self._reduction
 
     @property
     def reduction_arg(self):
-        ''' if this kernel/builtin contains a reduction variable then return
-        the variable, otherwise return None'''
+        '''
+        :returns: the reduction variable if this kernel/built-in
+        contains one and `None` otherwise.
+        :rtype: :py:class:`psyclone.psyGen.KernelArgument` or `NoneType`
+
+        '''
         return self._reduction_arg
 
     @property
     def reprod_reduction(self):
-        '''Determine whether this kernel/builtin is enclosed within an OpenMP
+        '''
+        :returns: whether this kernel/built-in is enclosed within an OpenMP
         do loop. If so report whether it has the reproducible flag
         set. Note, this also catches OMPParallelDo Directives but they
-        have reprod set to False so it is OK.'''
+        have reprod set to False so it is OK.
+        :rtype: bool
+
+        '''
         ancestor = self.ancestor(OMPDoDirective)
         if ancestor:
             return ancestor.reprod
@@ -1130,13 +1143,17 @@ class Kern(Statement):
 
     @property
     def local_reduction_name(self):
-        '''Generate a local variable name that is unique for the current
-        reduction argument name. This is used for thread-local
-        reductions with reproducible reductions '''
-        tag = self._reduction_arg.name
-        name = self.ancestor(InvokeSchedule).symbol_table.\
-            find_or_create_tag(tag, "l_" + tag).name
-        return name
+        '''
+        :returns: a local reduction variable name that is unique for the
+                  current reduction argument name. This is used for
+                  thread-local reductions with reproducible reductions.
+        :rtype: str
+
+        '''
+        # TODO #2381: Revisit symbol creation, now moved to the
+        # Kern._reduction_reference() method, and try to associate it
+        # with the PSy-layer generation or relevant transformation.
+        return "l_" + self.reduction_arg.name
 
     def zero_reduction_variable(self, parent, position=None):
         '''
@@ -1218,7 +1235,13 @@ class Kern(Statement):
         '''
         var_name = self._reduction_arg.name
         local_var_name = self.local_reduction_name
-        local_var_ref = self._reduction_ref(var_name)
+        # A non-reproducible reduction requires a single-valued argument
+        local_var_ref = self._reduction_reference().name
+        # A reproducible reduction requires multi-valued argument stored
+        # as a padded array separately for each thread
+        if self.reprod_reduction:
+            local_var_ref = FortranWriter().arrayreference_node(
+                self._reduction_reference())
         reduction_access = self._reduction_arg.access
         try:
             reduction_operator = REDUCTION_OPERATOR_MAPPING[reduction_access]
@@ -1239,22 +1262,40 @@ class Kern(Statement):
         parent.add(do_loop)
         parent.add(DeallocateGen(parent, local_var_name))
 
-    def _reduction_ref(self, name):
-        '''Return the name unchanged if OpenMP is set to be unreproducible, as
-        we will be using the OpenMP reduction clause. Otherwise we
-        will be computing the reduction ourselves and therefore need
-        to store values into a (padded) array separately for each
+    def _reduction_reference(self):
+        '''
+        Return the reference to the reduction variable if OpenMP is set to
+        be unreproducible, as we will be using the OpenMP reduction clause.
+        Otherwise we will be computing the reduction ourselves and therefore
+        need to store values into a (padded) array separately for each
         thread.
 
-        :param str name: original name of the variable to be reduced.
+        :returns: reference to the variable to be reduced.
+        :rtype: :py:class:`psyclone.psyir.nodes.Reference` or
+                :py:class:`psyclone.psyir.nodes.ArrayReference`
 
         '''
+        # TODO #2381: Revisit symbol creation, moved from the
+        # Kern.local_reduction_name property, and try to associate it
+        # with the PSy-layer generation or relevant transformation.
         symtab = self.scope.symbol_table
+        reduction_name = self.reduction_arg.name
+        # Return a multi-valued ArrayReference for a reproducible reduction
         if self.reprod_reduction:
-            idx_name = symtab.lookup_with_tag("omp_thread_index").name
-            local_name = symtab.find_or_create_tag(name, "l_" + name).name
-            return local_name + "(1," + idx_name + ")"
-        return name
+            array_dim = [
+                Literal("1", INTEGER_TYPE),
+                Reference(symtab.lookup_with_tag("omp_thread_index"))]
+            reduction_array = ArrayType(
+                symtab.lookup(reduction_name).datatype, array_dim)
+            local_reduction = DataSymbol(
+                self.local_reduction_name, datatype=reduction_array)
+            symtab.find_or_create_tag(
+                tag=self.local_reduction_name,
+                symbol_type=DataSymbol, datatype=reduction_array)
+            return ArrayReference.create(
+                local_reduction, array_dim)
+        # Return a single-valued Reference for a non-reproducible reduction
+        return Reference(symtab.lookup(reduction_name))
 
     @property
     def arg_descriptors(self):


Test patch:
diff --git a/src/psyclone/tests/domain/lfric/builtins/int_x_test.py b/src/psyclone/tests/domain/lfric/builtins/int_x_test.py
new file mode 100644
index 000000000..ca5952ce8
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/int_x_test.py
@@ -0,0 +1,176 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2017-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
+# Modified: I. Kavcic, Met Office
+# Modified: R. W. Ford and N. Nobre, STFC Daresbury Lab
+# Modified: by J. Henrichs, Bureau of Meteorology
+
+''' Module containing pytest tests of the LFRicIntXKern built-in
+    (converting real-valued to integer-valued field elements).'''
+
+import os
+import pytest
+
+from psyclone.configuration import Config
+from psyclone.domain.lfric.kernel import LFRicKernelMetadata
+from psyclone.domain.lfric.lfric_builtins import LFRicIntXKern
+from psyclone.parse.algorithm import parse
+from psyclone.psyGen import PSyFactory
+from psyclone.psyir.nodes import Assignment, Loop
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+def test_int_x(tmpdir, monkeypatch, annexed, dist_mem):
+    '''
+    Test that 1) the '__str__' method of 'LFRicIntXKern' returns the
+    expected string and 2) we generate correct code for the built-in
+    operation 'Y = INT(X, kind=i_<prec>)' where 'Y' is an integer-valued
+    field of kind 'i_<prec>' and 'X' is the real-valued field being
+    converted. Test with and without annexed DoFs being computed as this
+    affects the generated code. 3) Also test the 'metadata()' method.
+
+    '''
+    # Test metadata
+    metadata = LFRicIntXKern.metadata()
+    assert isinstance(metadata, LFRicKernelMetadata)
+    api_config = Config.get().api_conf(API)
+    # Test with and without annexed DoFs
+    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.10.3_int_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+
+    # Test '__str__' method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == ("Built-in: int_X (convert a real-valued to an "
+                         "integer-valued field)")
+
+    # Test code generation
+    code = str(psy.gen)
+
+    # Check that the correct field types and constants are used
+    output = (
+        "    USE constants_mod, ONLY: r_def, i_def\n"
+        "    USE field_mod, ONLY: field_type, field_proxy_type\n"
+        "    USE integer_field_mod, ONLY: integer_field_type, "
+        "integer_field_proxy_type\n")
+    assert output in code
+
+    # Check built-in loop
+    output = (
+        "      DO df=loop0_start,loop0_stop\n"
+        "        f2_data(df) = INT(f1_data(df), kind=i_def)\n"
+        "      END DO\n")
+    assert output in code
+
+    if not dist_mem:
+        assert "undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n" in code
+        assert "loop0_stop = undf_aspc1_f2\n" in code
+    else:
+        output_dm = "loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
+        assert output in code
+        assert "CALL f2_proxy%set_dirty()\n" in code
+        if not annexed:
+            output_dm = output_dm.replace("dof_annexed", "dof_owned")
+        assert output_dm in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+@pytest.mark.parametrize("kind_name", ["i_native", "i_ncdf"])
+def test_int_x_precision(monkeypatch, kind_name):
+    '''
+    Test that the built-in picks up and creates correct code for field
+    data with precision that is not the default, i.e. not 'i_def'.
+    At the moment there is no other integer precision for field data
+    so we use random integer precisions from 'constants_mod'.
+    However, this does mean that we are not able to check whether the
+    generated PSy layer compiles.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.10.3_int_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    table = first_invoke.schedule.symbol_table
+    arg = first_invoke.schedule.children[0].loop_body[0].args[0]
+    # Set 'f2_data' to another 'i_<prec>'
+    sym_kern = table.lookup_with_tag(f"{arg.name}:data")
+    monkeypatch.setattr(arg, "_precision", f"{kind_name}")
+    monkeypatch.setattr(sym_kern.datatype.partial_datatype.precision,
+                        "_name", f"{kind_name}")
+
+    # Test limited code generation (no equivalent field type)
+    code = str(psy.gen)
+    assert f"USE constants_mod, ONLY: r_def, {kind_name}" in code
+    assert (f"INTEGER(KIND={kind_name}), pointer, dimension(:) :: "
+            "f2_data => null()") in code
+    assert f"f2_data(df) = INT(f1_data(df), kind={kind_name})" in code
+
+
+def test_int_x_lowering(fortran_writer):
+    '''
+    Test that the lower_to_language_level() method works as expected.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.10.3_int_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=False).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    parent = kern.parent
+    lowered = kern.lower_to_language_level()
+    assert parent.children[0] is lowered
+    assert isinstance(parent.children[0], Assignment)
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  f2_data(df) = INT(f1_data(df), kind=i_def)\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/builtins/real_x_test.py b/src/psyclone/tests/domain/lfric/builtins/real_x_test.py
new file mode 100644
index 000000000..5448f99c2
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/real_x_test.py
@@ -0,0 +1,188 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2021-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.
+# -----------------------------------------------------------------------------
+# Authors: I. Kavcic, Met Office
+#          A. R. Porter, STFC Daresbury Laboratory
+# Modified: R. W. Ford, STFC Daresbury Lab
+
+''' Module containing pytest tests of the LFRicRealXKern built-in
+    (converting integer-valued to real-valued field elements).'''
+
+import os
+import re
+import pytest
+
+from psyclone.configuration import Config
+from psyclone.domain.lfric.kernel import LFRicKernelMetadata
+from psyclone.domain.lfric.lfric_builtins import LFRicRealXKern
+from psyclone.parse.algorithm import parse
+from psyclone.psyGen import PSyFactory
+from psyclone.psyir.nodes import Assignment, Loop
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+def test_real_x(tmpdir, monkeypatch, annexed, dist_mem):
+    '''
+    Test that 1) the '__str__' method of 'LFRicRealXKern' returns the
+    expected string and 2) we generate correct code for the built-in
+    operation 'Y = REAL(X, kind=r_<prec>)' where 'Y' is a real-valued
+    field of precision 'r_<prec>', 'X' is the integer-valued field being
+    converted. Test with and without annexed DoFs being computed as this
+    affects the generated code. 3) Also test the 'metadata()' method.
+
+    '''
+    # Test metadata
+    metadata = LFRicRealXKern.metadata()
+    assert isinstance(metadata, LFRicKernelMetadata)
+    # Test with and without annexed DoFs
+    api_config = Config.get().api_conf(API)
+    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.28.2_real_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+
+    # Test '__str__' method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == ("Built-in: real_X (convert an integer-valued to a "
+                         "real-valued field)")
+    # Test code generation
+    code = str(psy.gen)
+
+    # Check that the correct field types and constants are used
+    output = (
+        "    USE constants_mod, ONLY: r_def, i_def\n"
+        "    USE field_mod, ONLY: field_type, field_proxy_type\n"
+        "    USE integer_field_mod, ONLY: integer_field_type, "
+        "integer_field_proxy_type\n")
+    assert output in code
+
+    # Check built-in loop
+    output = (
+        "      DO df=loop0_start,loop0_stop\n"
+        "        f2_data(df) = REAL(f1_data(df), kind=r_def)\n"
+        "      END DO\n")
+    assert output in code
+
+    if not dist_mem:
+        assert "undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n" in code
+        assert "loop0_stop = undf_aspc1_f2\n" in code
+    else:
+        output_dm = "loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
+        assert output in code
+        assert "CALL f2_proxy%set_dirty()\n" in code
+        if not annexed:
+            output_dm = output_dm.replace("dof_annexed", "dof_owned")
+        assert output_dm in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+@pytest.mark.parametrize("kind_name", ["r_solver", "r_tran", "r_bl", "r_phys"])
+def test_real_x_precision(tmpdir, kind_name):
+    '''
+    Test that the built-in picks up and creates correct code for field
+    data with precision that is not the default, i.e. not 'r_def'. Try with
+    all supported LFRic real-valued field precisions to make sure all work.
+
+    '''
+    # Note that monkeypatching required to change field type, field proxy
+    # type and the field data precisions is extensive and complicated.
+    # Modifying the test algorithm is easier and more effective.
+    with open(os.path.join(BASE_PATH, "15.28.2_real_X_builtin.f90"),
+              "r", encoding='utf-8') as alg_file:
+        alg_code = alg_file.read()
+
+    # Modify the 'real'-valued field type and precision, and store the
+    # modified temporary algorithm
+    pattern = re.compile(r"\bfield_")
+    alg_code = re.sub(pattern, f"{kind_name}_field_", alg_code)
+    os.mkdir(str(tmpdir.join("tmp")))
+    tmp_fname = str(tmpdir.join("tmp", f"real_{kind_name}_X_builtin_alg.f90"))
+    with open(tmp_fname, "w", encoding='utf-8') as tmp_file:
+        tmp_file.write(alg_code)
+    tmp_file.close()
+
+    # Read and parse the modified algorithm
+    with open(tmp_fname, "r", encoding='utf-8') as alg_file:
+        _, invoke_info = parse(alg_file, api=API)
+    psy = PSyFactory(API).create(invoke_info)
+    code = str(psy.gen)
+
+    # Test code generation
+    assert f"USE constants_mod, ONLY: {kind_name}, i_def" in code
+    assert (f"USE {kind_name}_field_mod, ONLY: {kind_name}_field_type, "
+            f"{kind_name}_field_proxy_type") in code
+    assert f"TYPE({kind_name}_field_type), intent(in) :: f2" in code
+    assert (f"REAL(KIND={kind_name}), pointer, dimension(:) :: "
+            "f2_data => null()") in code
+    assert f"TYPE({kind_name}_field_proxy_type) f2_proxy" in code
+    assert f"f2_data(df) = REAL(f1_data(df), kind={kind_name})" in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+def test_real_x_lowering(fortran_writer):
+    '''
+    Test that the lower_to_language_level() method works as expected.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.28.2_real_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=False).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    parent = kern.parent
+    lowered = kern.lower_to_language_level()
+    assert parent.children[0] is lowered
+    assert isinstance(parent.children[0], Assignment)
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  f2_data(df) = REAL(f1_data(df), kind=r_def)\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/builtins/reduction_builtins_test.py b/src/psyclone/tests/domain/lfric/builtins/reduction_builtins_test.py
new file mode 100644
index 000000000..e909b7644
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/reduction_builtins_test.py
@@ -0,0 +1,208 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2017-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
+# Modified: I. Kavcic, Met Office
+# Modified: R. W. Ford and N. Nobre, STFC Daresbury Lab
+# Modified: by J. Henrichs, Bureau of Meteorology
+
+''' Module containing pytest tests of the LFRic general reduction built-in.'''
+
+import os
+import pytest
+
+from psyclone.configuration import Config
+from psyclone.domain.lfric import lfric_builtins
+from psyclone.parse.algorithm import parse
+from psyclone.parse.utils import ParseError
+from psyclone.psyGen import PSyFactory
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+# ------------- Invalid built-in with an integer scalar reduction ----------- #
+
+def test_scalar_int_builtin_error(monkeypatch):
+    '''
+    Test that specifying incorrect meta-data for built-in such that it
+    claims to perform a reduction into an integer variable raises the
+    expected error.
+
+    '''
+    monkeypatch.setattr(lfric_builtins, "BUILTIN_DEFINITIONS_FILE",
+                        value=os.path.join(BASE_PATH,
+                                           "int_reduction_builtins_mod.f90"))
+    with pytest.raises(ParseError) as excinfo:
+        _, _ = parse(os.path.join(BASE_PATH,
+                                  "16.2_integer_scalar_sum.f90"),
+                     api=API)
+    assert ("In the LFRic API a reduction access 'gh_sum' is only valid "
+            "with a real scalar argument, but a scalar argument with "
+            "'gh_integer' data type was found" in str(excinfo.value))
+
+
+# ------------- Built-ins with reductions ----------------------------------- #
+
+
+def test_multi_builtin_single_invoke(tmpdir, monkeypatch, annexed, dist_mem):
+    '''
+    Test that multiple built-ins, including one with reductions,
+    produce correct code. Also test with and without annexed DoFs
+    being computed as this affects the generated code.
+
+    '''
+    # Test with and without annexed DoFs
+    api_config = Config.get().api_conf(API)
+    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
+    _, invoke_info = parse(
+        os.path.join(BASE_PATH,
+                     "15.18.1_builtins_reduction_fuse_error.f90"),
+        api=API)
+    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    # Test code generation
+    code = str(psy.gen)
+
+    if dist_mem:
+        assert (
+            "    SUBROUTINE invoke_0(asum, f1, f2, b)\n"
+            "      USE scalar_mod, ONLY: scalar_type\n"
+            "      USE mesh_mod, ONLY: mesh_type\n"
+            "      REAL(KIND=r_def), intent(out) :: asum\n"
+            "      REAL(KIND=r_def), intent(in) :: b\n"
+            "      TYPE(field_type), intent(in) :: f1, f2\n"
+            "      TYPE(scalar_type) global_sum\n"
+            "      INTEGER df\n"
+            "      INTEGER(KIND=i_def) loop2_start, loop2_stop\n"
+            "      INTEGER(KIND=i_def) loop1_start, loop1_stop\n"
+            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
+            "      REAL(KIND=r_def), pointer, dimension(:) :: f2_data => "
+            "null()\n"
+            "      REAL(KIND=r_def), pointer, dimension(:) :: f1_data => "
+            "null()\n"
+            "      TYPE(field_proxy_type) f1_proxy, f2_proxy\n") in code
+        assert (
+            "      f1_proxy = f1%get_proxy()\n"
+            "      f1_data => f1_proxy%data\n"
+            "      f2_proxy = f2%get_proxy()\n"
+            "      f2_data => f2_proxy%data\n"
+            "      !\n") in code
+        output = (
+            "      loop2_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
+            "      !\n"
+            "      ! Call kernels and communication routines\n"
+            "      !\n"
+            "      !\n"
+            "      ! Zero summation variables\n"
+            "      !\n"
+            "      asum = 0.0_r_def\n"
+            "      !\n"
+            "      DO df=loop0_start,loop0_stop\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
+            "      END DO\n"
+            "      global_sum%value = asum\n"
+            "      asum = global_sum%get_sum()\n"
+            "      DO df=loop1_start,loop1_stop\n"
+            "        f1_data(df) = b * f1_data(df)\n"
+            "      END DO\n"
+            "      !\n"
+            "      ! Set halos dirty/clean for fields modified in the "
+            "above loop\n"
+            "      !\n"
+            "      CALL f1_proxy%set_dirty()\n"
+            "      !\n"
+            "      DO df=loop2_start,loop2_stop\n"
+            "        f1_data(df) = asum * f1_data(df)\n"
+            "      END DO\n"
+            "      !\n"
+            "      ! Set halos dirty/clean for fields modified in the "
+            "above loop\n"
+            "      !\n"
+            "      CALL f1_proxy%set_dirty()\n")
+        if not annexed:
+            output = output.replace("dof_annexed", "dof_owned")
+        assert output in code
+    else:
+        assert (
+            "    SUBROUTINE invoke_0(asum, f1, f2, b)\n"
+            "      REAL(KIND=r_def), intent(out) :: asum\n"
+            "      REAL(KIND=r_def), intent(in) :: b\n"
+            "      TYPE(field_type), intent(in) :: f1, f2\n"
+            "      INTEGER df\n"
+            "      INTEGER(KIND=i_def) loop2_start, loop2_stop\n"
+            "      INTEGER(KIND=i_def) loop1_start, loop1_stop\n"
+            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
+            "      REAL(KIND=r_def), pointer, dimension(:) :: f2_data => "
+            "null()\n"
+            "      REAL(KIND=r_def), pointer, dimension(:) :: f1_data => "
+            "null()\n"
+            "      TYPE(field_proxy_type) f1_proxy, f2_proxy\n"
+            "      INTEGER(KIND=i_def) undf_aspc1_f1\n") in code
+        assert (
+            "      f1_proxy = f1%get_proxy()\n"
+            "      f1_data => f1_proxy%data\n"
+            "      f2_proxy = f2%get_proxy()\n"
+            "      f2_data => f2_proxy%data\n"
+            "      !\n"
+            "      ! Initialise number of DoFs for aspc1_f1\n"
+            "      !\n"
+            "      undf_aspc1_f1 = "
+            "f1_proxy%vspace%get_undf()\n" in code)
+        assert (
+            "      loop0_stop = undf_aspc1_f1\n"
+            "      loop1_start = 1\n"
+            "      loop1_stop = undf_aspc1_f1\n"
+            "      loop2_start = 1\n"
+            "      loop2_stop = undf_aspc1_f1\n" in code)
+        assert (
+            "      asum = 0.0_r_def\n"
+            "      !\n"
+            "      DO df=loop0_start,loop0_stop\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
+            "      END DO\n"
+            "      DO df=loop1_start,loop1_stop\n"
+            "        f1_data(df) = b * f1_data(df)\n"
+            "      END DO\n"
+            "      DO df=loop2_start,loop2_stop\n"
+            "        f1_data(df) = asum * f1_data(df)\n"
+            "      END DO\n") in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
diff --git a/src/psyclone/tests/domain/lfric/builtins/setval_random_test.py b/src/psyclone/tests/domain/lfric/builtins/setval_random_test.py
index ac61ca5f2..1535476ae 100644
--- a/src/psyclone/tests/domain/lfric/builtins/setval_random_test.py
+++ b/src/psyclone/tests/domain/lfric/builtins/setval_random_test.py
@@ -33,6 +33,7 @@
 # -----------------------------------------------------------------------------
 # Author: A. R. Porter, STFC Daresbury Lab
 # Modified: S. Siso and R. W. Ford, STFC Daresbury Lab
+#           I. Kavcic, Met Office
 
 '''Module containing pytest tests of the LFRicSetvalRandomKern
 built-in.
@@ -44,7 +45,7 @@ from psyclone.domain.lfric.kernel import LFRicKernelMetadata
 from psyclone.domain.lfric.lfric_builtins import LFRicSetvalRandomKern
 from psyclone.parse.algorithm import parse
 from psyclone.psyGen import PSyFactory
-from psyclone.psyir.nodes import IntrinsicCall
+from psyclone.psyir.nodes import IntrinsicCall, Loop
 from psyclone.tests.lfric_build import LFRicBuild
 
 
@@ -70,6 +71,7 @@ def test_setval_random(tmpdir):
                            api=API)
     psy = PSyFactory(API,
                      distributed_memory=True).create(invoke_info)
+
     # Test 'str()' method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
@@ -79,17 +81,18 @@ def test_setval_random(tmpdir):
             "pseudo-random numbers)")
 
     # Test code generation
-    code = str(psy.gen).lower()
-
+    code = str(psy.gen)
     output = (
-        "      do df=loop0_start,loop0_stop\n"
-        "        call random_number(f1_data(df))\n"
-        "      end do\n")
+        "      DO df=loop0_start,loop0_stop\n"
+        "        CALL RANDOM_NUMBER(f1_data(df))\n"
+        "      END DO\n")
     assert output in code
+
+    # Test compilation of generated code
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
 
-def test_setval_random_lowering():
+def test_setval_random_lowering(fortran_writer):
     '''Test that the 'lower_to_language_level()' method works as
     expected.
 
@@ -98,7 +101,7 @@ def test_setval_random_lowering():
                                         "15.7.4_setval_random_builtin.f90"),
                            api=API)
     psy = PSyFactory(API,
-                     distributed_memory=False).create(invoke_info)
+                     distributed_memory=True).create(invoke_info)
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     parent = kern.parent
@@ -106,3 +109,9 @@ def test_setval_random_lowering():
     assert parent.children[0] is lowered
     assert isinstance(parent.children[0], IntrinsicCall)
     assert parent.children[0].routine.name == "RANDOM_NUMBER"
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  call RANDOM_NUMBER(f1_data(df))\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/builtins/sum_x_test.py b/src/psyclone/tests/domain/lfric/builtins/sum_x_test.py
new file mode 100644
index 000000000..136ce9e90
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/sum_x_test.py
@@ -0,0 +1,130 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2017-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
+# Modified: I. Kavcic, Met Office
+# Modified: R. W. Ford and N. Nobre, STFC Daresbury Lab
+# Modified: by J. Henrichs, Bureau of Meteorology
+
+''' Module containing pytest tests of the LFRicSumXKern built-in.'''
+
+import os
+from psyclone.domain.lfric.kernel import LFRicKernelMetadata
+from psyclone.domain.lfric.lfric_builtins import LFRicSumXKern
+from psyclone.parse.algorithm import parse
+from psyclone.psyGen import PSyFactory
+from psyclone.psyir.nodes import Assignment, Loop
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+def test_sum_x(tmpdir, dist_mem):
+    '''
+    Test that 1) the '__str__' method of 'LFRicSumXKern' returns the
+    expected string and 2) we generate correct code for the built-in
+    operation which sums elements of a real-valued field 'X' as
+    'sumfld = sum(X(:))'. 3) Also test the 'metadata()' method.
+
+    '''
+    # Test metadata
+    metadata = LFRicSumXKern.metadata()
+    assert isinstance(metadata, LFRicKernelMetadata)
+    _, invoke_info = parse(
+        os.path.join(BASE_PATH,
+                     "15.8.1_sum_X_builtin.f90"), api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=dist_mem).create(invoke_info)
+
+    # Test '__str__' method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == "Built-in: sum_X (sum a real-valued field)"
+
+    # Test code generation
+    code = str(psy.gen)
+    assert "      REAL(KIND=r_def), intent(out) :: asum\n" in code
+    output = (
+            "      ! Zero summation variables\n"
+            "      !\n"
+            "      asum = 0.0_r_def\n"
+            "      !\n"
+            "      DO df=loop0_start,loop0_stop\n"
+            "        asum = asum + f1_data(df)\n"
+            "      END DO")
+    assert output in code
+
+    if not dist_mem:
+        assert "loop0_stop = undf_aspc1_f1\n" in code
+    else:
+        assert "USE scalar_mod, ONLY: scalar_type\n" in code
+        assert "TYPE(scalar_type) global_sum\n" in code
+        assert "loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n" in code
+        output = (
+            "      global_sum%value = asum\n"
+            "      asum = global_sum%get_sum()")
+        assert output in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+def test_sum_x_lowering(fortran_writer):
+    '''
+    Test that the lower_to_language_level() method works as expected.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.8.1_sum_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=True).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    parent = kern.parent
+    lowered = kern.lower_to_language_level()
+    assert parent.children[0] is lowered
+    assert isinstance(parent.children[0], Assignment)
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  asum = asum + f1_data(df)\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_x_test.py b/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_x_test.py
new file mode 100644
index 000000000..89a6e772b
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_x_test.py
@@ -0,0 +1,121 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2017-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
+# Modified: I. Kavcic, Met Office
+# Modified: R. W. Ford and N. Nobre, STFC Daresbury Lab
+# Modified: by J. Henrichs, Bureau of Meteorology
+
+''' Module containing pytest tests of the LFRicXInnerproductXKern built-ins
+    (inner product of a real-valued field with itself).'''
+
+import os
+from psyclone.domain.lfric.kernel import LFRicKernelMetadata
+from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductXKern
+from psyclone.parse.algorithm import parse
+from psyclone.psyGen import PSyFactory
+from psyclone.psyir.nodes import Assignment, Loop
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+def test_x_innerproduct_x(tmpdir, dist_mem):
+    '''
+    Test that 1) the '__str__' method of 'LFRicXInnerproductXKern'
+    returns the expected string and 2) we generate correct code
+    for the built-in operation which calculates inner product of
+    a real-valued field 'X' by itself as 'innprod = innprod +
+    X(:)*X(:)'. 3) Also test the 'metadata()' method.
+
+    '''
+    # Test metadata
+    metadata = LFRicXInnerproductXKern.metadata()
+    assert isinstance(metadata, LFRicKernelMetadata)
+    _, invoke_info = parse(
+        os.path.join(BASE_PATH,
+                     "15.9.2_X_innerproduct_X_builtin.f90"),
+        api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=dist_mem).create(invoke_info)
+
+    # Test '__str__' method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == "Built-in: X_innerproduct_X (real-valued field)"
+
+    # Test code generation
+    code = str(psy.gen)
+    output = (
+            "      ! Zero summation variables\n"
+            "      !\n"
+            "      asum = 0.0_r_def\n"
+            "      !\n"
+            "      DO df=loop0_start,loop0_stop\n"
+            "        asum = asum + f1_data(df) * f1_data(df)\n"
+            "      END DO")
+    assert output in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+def test_x_innerproduct_x_lowering(fortran_writer):
+    '''
+    Test that the lower_to_language_level() method of X_innerproduct_X
+    built-in works as expected.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                           "15.9.2_X_innerproduct_X_builtin.f90"), api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=True).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    parent = kern.parent
+    lowered = kern.lower_to_language_level()
+    assert parent.children[0] is lowered
+    assert isinstance(parent.children[0], Assignment)
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  asum = asum + f1_data(df) * f1_data(df)\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_y_test.py b/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_y_test.py
new file mode 100644
index 000000000..70d7a7119
--- /dev/null
+++ b/src/psyclone/tests/domain/lfric/builtins/x_innerproduct_y_test.py
@@ -0,0 +1,132 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2017-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
+# Modified: I. Kavcic, Met Office
+# Modified: R. W. Ford and N. Nobre, STFC Daresbury Lab
+# Modified: by J. Henrichs, Bureau of Meteorology
+
+''' Module containing pytest tests of the LFRicXInnerproductYKern built-in
+    (inner product of two real-valued fields).'''
+
+import os
+from psyclone.domain.lfric.kernel import LFRicKernelMetadata
+from psyclone.domain.lfric.lfric_builtins import LFRicXInnerproductYKern
+from psyclone.parse.algorithm import parse
+from psyclone.psyGen import PSyFactory
+from psyclone.psyir.nodes import Assignment, Loop
+from psyclone.tests.lfric_build import LFRicBuild
+
+# Constants
+BASE_PATH = os.path.join(
+    os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
+        os.path.abspath(__file__))))),
+    "test_files", "dynamo0p3")
+
+# The PSyclone API under test
+API = "dynamo0.3"
+
+
+def test_x_innerproduct_y(tmpdir, dist_mem):
+    '''
+    Test that 1) the '__str__' method of 'LFRicXInnerproductYKern'
+    returns the expected string and 2) we generate correct code
+    for the built-in operation which calculates inner product of
+    real-valued fields 'X' and 'Y' as 'innprod = innprod +
+    X(:)*Y(:)'. 3) Also test the 'metadata()' method.
+
+    '''
+    # Test metadata
+    metadata = LFRicXInnerproductYKern.metadata()
+    assert isinstance(metadata, LFRicKernelMetadata)
+    _, invoke_info = parse(
+        os.path.join(BASE_PATH,
+                     "15.9.1_X_innerproduct_Y_builtin.f90"),
+        api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=dist_mem).create(invoke_info)
+
+    # Test '__str__' method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == "Built-in: X_innerproduct_Y (real-valued fields)"
+
+    # Test code generation
+    code = str(psy.gen)
+    output = (
+            "      ! Zero summation variables\n"
+            "      !\n"
+            "      asum = 0.0_r_def\n"
+            "      !\n"
+            "      DO df=loop0_start,loop0_stop\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
+            "      END DO")
+    assert output in code
+
+    if not dist_mem:
+        assert "loop0_stop = undf_aspc1_f1\n" in code
+    else:
+        assert "USE scalar_mod, ONLY: scalar_type\n" in code
+        assert "TYPE(scalar_type) global_sum\n" in code
+        assert "loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n" in code
+        output = (
+            "      global_sum%value = asum\n"
+            "      asum = global_sum%get_sum()")
+        assert output in code
+
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+
+def test_x_innerproduct_y_lowering(fortran_writer):
+    '''
+    Test that the lower_to_language_level() method of X_innerproduct_Y
+    built-in works as expected.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                           "15.9.1_X_innerproduct_Y_builtin.f90"), api=API)
+    psy = PSyFactory(API,
+                     distributed_memory=True).create(invoke_info)
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    parent = kern.parent
+    lowered = kern.lower_to_language_level()
+    assert parent.children[0] is lowered
+    assert isinstance(parent.children[0], Assignment)
+
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    assert ("do df = loop0_start, loop0_stop, 1\n"
+            "  asum = asum + f1_data(df) * f2_data(df)\n"
+            "enddo") in code
diff --git a/src/psyclone/tests/domain/lfric/kern_call_acc_arg_list_test.py b/src/psyclone/tests/domain/lfric/kern_call_acc_arg_list_test.py
index 8b4d87eb6..e13d9260d 100644
--- a/src/psyclone/tests/domain/lfric/kern_call_acc_arg_list_test.py
+++ b/src/psyclone/tests/domain/lfric/kern_call_acc_arg_list_test.py
@@ -58,7 +58,8 @@ BASE_PATH = get_base_path(TEST_API)
 
 def test_acc_arg_list_cell_map(dist_mem, monkeypatch):
     '''Test the cell_map() method.'''
-    # We need a LFRicKern in order to construct an instance of KernCallAccArgList
+    # We need a LFRicKern in order to construct an instance of
+    # KernCallAccArgList
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "22.0_intergrid_prolong.f90"),
                            api=TEST_API)
diff --git a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
index ee13e067d..0877ff484 100644
--- a/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
+++ b/src/psyclone/tests/domain/lfric/lfric_builtins_test.py
@@ -52,7 +52,7 @@ from psyclone.core import Signature, VariablesAccessInfo
 from psyclone.domain.lfric import lfric_builtins, LFRicConstants
 from psyclone.domain.lfric.kernel import LFRicKernelMetadata, FieldArgMetadata
 from psyclone.domain.lfric.lfric_builtins import (
-    LFRicBuiltInCallFactory, LFRicBuiltIn, LFRicXKern)
+    LFRicBuiltInCallFactory, LFRicBuiltIn)
 from psyclone.dynamo0p3 import DynKernelArgument
 from psyclone.errors import GenerationError, InternalError
 from psyclone.parse.algorithm import BuiltInCall, parse
@@ -119,20 +119,6 @@ class Dummy3(Dummy2):
     _case_name = "test"
 
 
-class XKernDummy(LFRicXKern):
-    '''Utility class to test that LFRicXKern raises the expected
-    exception.
-
-    '''
-    _datatype = "xkerndummy"
-
-    @staticmethod
-    def metadata():
-        return None
-
-    def __str__(self):
-        return "dummy"
-
 # ------------- Tests for built-ins methods and arguments ------------------- #
 
 
@@ -200,38 +186,6 @@ def test_lfric_builtin_fs_descriptors():
     assert not instance.fs_descriptors
 
 
-# pylint: disable=invalid-name
-def test_lfricxkern_abstract():
-    '''Test that the 'LFRicXKern' class is abstract and that it sets its
-    internal '_field_type' variable to 'None'.
-
-    '''
-    with pytest.raises(TypeError) as error:
-        # pylint: disable=abstract-class-instantiated
-        lfric_builtins.LFRicXKern()
-    # Python >= 3.9 spots that 'method' should be singular. Prior to this it
-    # was plural. Python >= 3.12 tweaks the error message yet again to mention
-    # the lack of an implementation and to quote the method name.
-    # We split the check to accomodate for this.
-    assert ("Can't instantiate abstract class LFRicXKern with"
-            in str(error.value))
-    assert ("abstract method" in str(error.value))
-    assert ("metadata" in str(error.value))
-    assert lfric_builtins.LFRicXKern._field_type is None
-
-
-def test_lfricxkern_exception():
-    '''Test that 'LFRicXKern' raises an exception if it is subclassed and
-    the subclass does not set the variable '_field_type' to a value.
-
-    '''
-    dummy = XKernDummy()
-    with pytest.raises(InternalError) as info:
-        dummy.gen_code(None)
-    assert ("Subclasses of LFRicXKern must set the _field_type variable "
-            "to the output datatype." in str(info.value))
-
-
 def test_lfricbuiltin_missing_defs(monkeypatch):
     '''Check that we raise an appropriate error if we cannot find the file
     specifying meta-data for built-in kernels.
@@ -3649,242 +3603,6 @@ def test_setval_X(tmpdir, monkeypatch, annexed, dist_mem, fortran_writer):
             output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
         assert output_dm_2 in code
 
-# ------------- Inner product of real fields -------------------------------- #
-
-
-def test_X_innerproduct_Y(tmpdir, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicXInnerproductYKern'
-    returns the expected string and 2) we generate correct code for
-    the built-in operation which calculates inner product of
-    real-valued fields 'X' and 'Y' as 'innprod = innprod +
-    X(:)*Y(:)'. 3) Also test the 'metadata()' method.
-
-    '''
-    metadata = lfric_builtins.LFRicXInnerproductYKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    _, invoke_info = parse(
-        os.path.join(BASE_PATH,
-                     "15.9.1_X_innerproduct_Y_builtin.f90"),
-        api=API)
-    psy = PSyFactory(API,
-                     distributed_memory=dist_mem).create(invoke_info)
-    # Test '__str__' method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == "Built-in: X_innerproduct_Y (real-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    output = (
-        "      !\n"
-        "      ! Initialise field and/or operator proxies\n"
-        "      !\n"
-        "      f1_proxy = f1%get_proxy()\n"
-        "      f1_data => f1_proxy%data\n"
-        "      f2_proxy = f2%get_proxy()\n"
-        "      f2_data => f2_proxy%data\n"
-        "      !\n")
-    assert output in code
-    if not dist_mem:
-        output_seq = (
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
-            "      END DO\n"
-            "      !\n")
-        assert output_seq in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
-            "      END DO\n"
-            "      global_sum%value = asum\n"
-            "      asum = global_sum%get_sum()\n"
-            "      !\n")
-        assert output_dm in code
-        assert "      USE scalar_mod, ONLY: scalar_type" in code
-        assert "      REAL(KIND=r_def), intent(out) :: asum\n" in code
-        assert "      TYPE(scalar_type) global_sum\n" in code
-
-
-def test_X_innerproduct_X(tmpdir, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicXInnerproductXKern'
-    returns the expected string and 2) we generate correct code for
-    the built-in operation which calculates inner product of a
-    real-valued field 'X' by itself as 'innprod = innprod +
-    X(:)*X(:)'. 3) Also test the 'metadata()' method.
-
-    '''
-    metadata = lfric_builtins.LFRicXInnerproductXKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    _, invoke_info = parse(
-        os.path.join(BASE_PATH,
-                     "15.9.2_X_innerproduct_X_builtin.f90"),
-        api=API)
-    psy = PSyFactory(API,
-                     distributed_memory=dist_mem).create(invoke_info)
-    # Test '__str__' method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == "Built-in: X_innerproduct_X (real-valued field)"
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    output = (
-        "      !\n"
-        "      ! Initialise field and/or operator proxies\n"
-        "      !\n"
-        "      f1_proxy = f1%get_proxy()\n"
-        "      f1_data => f1_proxy%data\n"
-        "      !\n")
-    assert output in code
-    if not dist_mem:
-        output_seq = (
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f1_data(df)\n"
-            "      END DO\n"
-            "      !\n")
-        assert output_seq in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f1_data(df)\n"
-            "      END DO\n"
-            "      global_sum%value = asum\n"
-            "      asum = global_sum%get_sum()\n"
-            "      !\n")
-        assert output_dm in code
-        assert "      USE scalar_mod, ONLY: scalar_type" in code
-        assert "      REAL(KIND=r_def), intent(out) :: asum\n" in code
-        assert "      TYPE(scalar_type) global_sum\n" in code
-
-
-# ------------- Sum real field elements ------------------------------------- #
-
-
-def test_sum_X(tmpdir, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicSumXKern' returns the
-    expected string and 2) we generate correct code for the built-in
-    operation which sums elements of a real-valued field 'X' as
-    'sumfld = sum(X(:))'. 3) Also test the 'metadata()' method.
-
-    '''
-    metadata = lfric_builtins.LFRicSumXKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    _, invoke_info = parse(
-        os.path.join(BASE_PATH,
-                     "15.8.1_sum_X_builtin.f90"), api=API)
-    psy = PSyFactory(API,
-                     distributed_memory=dist_mem).create(invoke_info)
-    # Test '__str__' method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == "Built-in: sum_X (sum a real-valued field)"
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    output = (
-        "      !\n"
-        "      ! Initialise field and/or operator proxies\n"
-        "      !\n"
-        "      f1_proxy = f1%get_proxy()\n"
-        "      f1_data => f1_proxy%data\n"
-        "      !\n")
-    assert output in code
-    if not dist_mem:
-        output = (
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)\n"
-            "      END DO")
-        assert output in code
-    else:
-        output = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_owned()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)\n"
-            "      END DO\n"
-            "      global_sum%value = asum\n"
-            "      asum = global_sum%get_sum()")
-        assert output in code
-        assert "      REAL(KIND=r_def), intent(out) :: asum\n" in code
-
 
 # ------------- Sign of real field elements --------------------------------- #
 
@@ -4251,122 +3969,6 @@ def test_inc_min_aX(tmpdir, monkeypatch, annexed, dist_mem, fortran_writer):
         assert output_dm_2 in code
 
 
-# ------------- Converting real to integer field elements ------------------- #
-
-
-def test_int_X(tmpdir, monkeypatch, annexed, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicIntXKern' returns the
-    expected string and 2) we generate correct code for the built-in
-    operation 'Y = int(X, i_def)' where 'Y' is an integer-valued
-    field, 'X' is the real-valued field being converted and the
-    correct kind, 'i_def', is picked up from the associated
-    field. Test with and without annexed DoFs being computed as this
-    affects the generated code. 3) Also test the 'metadata()' method.
-
-    '''
-    metadata = lfric_builtins.LFRicIntXKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
-    _, invoke_info = parse(os.path.join(BASE_PATH,
-                                        "15.10.3_int_X_builtin.f90"),
-                           api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
-    # Test '__str__' method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == ("Built-in: int_X (convert a real-valued to an "
-                         "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    # First check that the correct field types and constants are used
-    output = (
-        "    USE constants_mod, ONLY: r_def, i_def\n"
-        "    USE field_mod, ONLY: field_type, field_proxy_type\n"
-        "    USE integer_field_mod, ONLY: integer_field_type, "
-        "integer_field_proxy_type\n")
-    assert output in code
-
-    if not dist_mem:
-        output = (
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f2_proxy\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(field_proxy_type) f1_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f2\n"
-            "      !\n"
-            "      undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = int(f1_data(df), i_def)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = int(f1_data(df), i_def)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
-
-def test_int_X_precision(monkeypatch):
-    '''Test that the built-in picks up and creates correct code for a
-    scalar with precision that is not the default i.e. not 'i_def'. At
-    the moment there is no other integer precision so we make one up
-    and use monkeypatch to get round any error checks. However, this
-    does mean that we can't check whether it compiles. 3) Also test
-    the 'metadata()' method.
-
-    '''
-    metadata = lfric_builtins.LFRicIncATimesXKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    _, invoke_info = parse(os.path.join(BASE_PATH,
-                                        "15.10.3_int_X_builtin.f90"),
-                           api=API)
-    psy = PSyFactory(API).create(invoke_info)
-    # Test '__str__' method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    monkeypatch.setattr(kern.args[0], "_precision", "i_solver")
-    code = str(psy.gen)
-    assert "USE constants_mod, ONLY: r_def, i_solver, i_def" in code
-    assert "f2_data(df) = int(f1_data(df), i_solver)" in code
-
 # ------------- Xfail built-ins --------------------------------------------- #
 
 
@@ -4963,152 +4565,6 @@ def test_builtin_set_plus_normal(tmpdir, monkeypatch, annexed, dist_mem):
         assert output_dm_2 in code
 
 
-# ------------- Built-ins with reductions ----------------------------------- #
-
-
-def test_multi_builtin_single_invoke(tmpdir, monkeypatch, annexed, dist_mem):
-    '''Test that multiple built-ins, including one with reductions,
-    produce correct code. Also test with and without annexed DoFs
-    being computed as this affects the generated code.
-
-    '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
-    _, invoke_info = parse(
-        os.path.join(BASE_PATH,
-                     "15.18.1_builtins_reduction_fuse_error.f90"),
-        api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    if dist_mem:
-        assert (
-            "    SUBROUTINE invoke_0(asum, f1, f2, b)\n"
-            "      USE scalar_mod, ONLY: scalar_type\n"
-            "      USE mesh_mod, ONLY: mesh_type\n"
-            "      REAL(KIND=r_def), intent(out) :: asum\n"
-            "      REAL(KIND=r_def), intent(in) :: b\n"
-            "      TYPE(field_type), intent(in) :: f1, f2\n"
-            "      TYPE(scalar_type) global_sum\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop2_start, loop2_stop\n"
-            "      INTEGER(KIND=i_def) loop1_start, loop1_stop\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(field_proxy_type) f1_proxy, f2_proxy\n") in code
-        assert (
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n") in code
-        output = (
-            "      loop2_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      !\n"
-            "      ! Zero summation variables\n"
-            "      !\n"
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
-            "      END DO\n"
-            "      global_sum%value = asum\n"
-            "      asum = global_sum%get_sum()\n"
-            "      DO df=loop1_start,loop1_stop\n"
-            "        f1_data(df) = b * f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n"
-            "      DO df=loop2_start,loop2_stop\n"
-            "        f1_data(df) = asum * f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n")
-        if not annexed:
-            output = output.replace("dof_annexed", "dof_owned")
-        assert output in code
-    else:
-        assert (
-            "    SUBROUTINE invoke_0(asum, f1, f2, b)\n"
-            "      REAL(KIND=r_def), intent(out) :: asum\n"
-            "      REAL(KIND=r_def), intent(in) :: b\n"
-            "      TYPE(field_type), intent(in) :: f1, f2\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop2_start, loop2_stop\n"
-            "      INTEGER(KIND=i_def) loop1_start, loop1_stop\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(field_proxy_type) f1_proxy, f2_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f1\n") in code
-        assert (
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = "
-            "f1_proxy%vspace%get_undf()\n" in code)
-        assert (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      loop1_start = 1\n"
-            "      loop1_stop = undf_aspc1_f1\n"
-            "      loop2_start = 1\n"
-            "      loop2_stop = undf_aspc1_f1\n" in code)
-        assert (
-            "      asum = 0.0_r_def\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
-            "      END DO\n"
-            "      DO df=loop1_start,loop1_stop\n"
-            "        f1_data(df) = b * f1_data(df)\n"
-            "      END DO\n"
-            "      DO df=loop2_start,loop2_stop\n"
-            "        f1_data(df) = asum * f1_data(df)\n"
-            "      END DO\n") in code
-
-
-# ------------- Invalid built-in with an integer scalar reduction ----------- #
-
-
-def test_scalar_int_builtin_error(monkeypatch):
-    '''Test that specifying incorrect meta-data for built-in such that it
-    claims to perform a reduction into an integer variable raises the
-    expected error.
-
-    '''
-    monkeypatch.setattr(lfric_builtins, "BUILTIN_DEFINITIONS_FILE",
-                        value=os.path.join(BASE_PATH,
-                                           "int_reduction_builtins_mod.f90"))
-    with pytest.raises(ParseError) as excinfo:
-        _, _ = parse(os.path.join(BASE_PATH,
-                                  "16.2_integer_scalar_sum.f90"),
-                     api=API)
-    assert ("In the LFRic API a reduction access 'gh_sum' is only valid "
-            "with a real scalar argument, but a scalar argument with "
-            "'gh_integer' data type was found" in str(excinfo.value))
-
-
 # ------------- Auxiliary mesh code generation function --------------------- #
 
 
diff --git a/src/psyclone/tests/domain/lfric/lfric_config_test.py b/src/psyclone/tests/domain/lfric/lfric_config_test.py
index e79fc7b19..5552083bf 100644
--- a/src/psyclone/tests/domain/lfric/lfric_config_test.py
+++ b/src/psyclone/tests/domain/lfric/lfric_config_test.py
@@ -234,7 +234,7 @@ def test_invalid_precision_map(tmpdir):
 
     assert ("Wrong type supplied to mapping: '-8' is not a positive"
             " integer or contains special characters." in str(err.value))
-    
+
     # Test invalid datatype: letter string
     content = re.sub(r"r_double: 8,", "r_double: number 5,",
                      _CONFIG_CONTENT,
@@ -245,7 +245,7 @@ def test_invalid_precision_map(tmpdir):
 
     assert ("Wrong type supplied to mapping: 'number 5' is not a positive"
             " integer or contains special characters." in str(err.value))
-        
+
     # Test invalid datatype: float
     content = re.sub(r"r_double: 8,", "r_double: 8.5,",
                      _CONFIG_CONTENT,
@@ -267,7 +267,7 @@ def test_invalid_precision_map(tmpdir):
 
     assert ("Wrong type supplied to mapping: '\u00b2' is not a positive"
             " integer or contains special characters." in str(err.value))
-    
+
 
 def test_invalid_num_any_anyd_spaces(tmpdir):
     ''' Check that we raise an error if we supply an invalid number
@@ -336,11 +336,11 @@ def test_default_kind():
     assert api_config.default_kind["real"] == "r_def"
     assert api_config.default_kind["integer"] == "i_def"
     assert api_config.default_kind["logical"] == "l_def"
-    
+
 
 def test_precision_map():
     '''Check that we load correct precision values for all
-    datatypes. 
+    datatypes.
 
     '''
     api_config = Config().get().api_conf(TEST_API)
diff --git a/src/psyclone/tests/domain/lfric/lfric_extract_driver_creator_test.py b/src/psyclone/tests/domain/lfric/lfric_extract_driver_creator_test.py
index 346cff417..e1da19142 100644
--- a/src/psyclone/tests/domain/lfric/lfric_extract_driver_creator_test.py
+++ b/src/psyclone/tests/domain/lfric/lfric_extract_driver_creator_test.py
@@ -396,53 +396,6 @@ def test_lfric_driver_operator():
     build.compile_file("driver-operator-test.F90")
 
 
-# ----------------------------------------------------------------------------
-@pytest.mark.parametrize("name, filename",
-                         [("x_innerproduct_x",
-                           "15.9.2_X_innerproduct_X_builtin.f90"),
-                          ("sum_x", "15.14.3_sum_setval_field_builtin.f90"),
-                          ("int_x", "15.10.3_int_X_builtin.f90"),
-                          ("real_x", "15.28.2_real_X_builtin.f90")
-                          ])
-def test_lfric_driver_unsupported_builtins(name, filename, capsys):
-    '''The following builtins do not have a proper lower_to_language_level
-    method to create the PSyIR, so they are not supported in the driver
-    creation: LFRicXInnerproductXKern, LFRicSumXKern, LFRicIntXKern,
-    LFRicRealXKern. This tests also the error handling of the functions
-    write_driver, get_driver_as_string, create. '''
-
-    _, invoke = get_invoke(filename, API, dist_mem=False, idx=0)
-
-    driver_creator = LFRicExtractDriverCreator()
-    read_write_info = ReadWriteInfo()
-
-    # The create method should raise an exception
-    # -------------------------------------------
-    with pytest.raises(NotImplementedError) as err:
-        # The parameters do not really matter
-        driver_creator.create(invoke.schedule, read_write_info, "extract",
-                              "_post", region_name=("region", "name"))
-    assert f"LFRic builtin '{name}' is not supported" in str(err.value)
-
-    # The get_driver_as_string method returns
-    # an empty string, but prints an error message
-    # --------------------------------------------
-    code = driver_creator.get_driver_as_string(invoke.schedule,
-                                               read_write_info, "extract",
-                                               "_post", ("region", "name"))
-    assert code == ""
-    out, _ = capsys.readouterr()
-    assert (f"Cannot create driver for 'region-name' because:\nLFRic builtin "
-            f"'{name}' is not supported" in out)
-
-    # write_driver prints an error message, and does not return an error
-    # ------------------------------------------------------------------
-    driver_creator.write_driver(invoke.schedule, read_write_info,
-                                "extract", "_post", ("region", "name"))
-    assert (f"Cannot create driver for 'region-name' because:\nLFRic builtin "
-            f"'{name}' is not supported" in out)
-
-
 # ----------------------------------------------------------------------------
 @pytest.mark.usefixtures("change_into_tmpdir", "init_module_manager")
 def test_lfric_driver_removing_structure_data():
diff --git a/src/psyclone/tests/domain/lfric/lfric_integer_builtins_test.py b/src/psyclone/tests/domain/lfric/lfric_integer_builtins_test.py
index 70870b2c6..964961ace 100644
--- a/src/psyclone/tests/domain/lfric/lfric_integer_builtins_test.py
+++ b/src/psyclone/tests/domain/lfric/lfric_integer_builtins_test.py
@@ -44,14 +44,11 @@
 '''
 
 import os
-import pytest
 
 from psyclone.configuration import Config
-from psyclone.domain.lfric.kernel import LFRicKernelMetadata
-from psyclone.domain.lfric.lfric_builtins import LFRicRealXKern
 from psyclone.parse.algorithm import parse
 from psyclone.psyGen import PSyFactory
-
+from psyclone.psyir.nodes import Loop
 from psyclone.tests.lfric_build import LFRicBuild
 
 # Constants
@@ -82,16 +79,23 @@ def test_int_X_plus_Y(tmpdir, monkeypatch, annexed, dist_mem):
                            api=API)
     psy = PSyFactory(API,
                      distributed_memory=dist_mem).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_X_plus_Y (add integer-valued fields)"
+
     # Test code generation
     code = str(psy.gen)
 
-    assert LFRicBuild(tmpdir).code_compiles(psy)
+    # Check for the correct 'use' module statements
+    output_mod = (
+        "    USE constants_mod, ONLY: i_def\n"
+        "    USE integer_field_mod, ONLY: integer_field_type,"
+        " integer_field_proxy_type\n")
+    assert output_mod in code
 
-    # Check for the correct field type declarations
+    # Check for the correct declarations
     output = (
         "      TYPE(integer_field_type), intent(in) :: f3, f1, f2\n"
         "      INTEGER df\n"
@@ -151,1424 +155,676 @@ def test_int_X_plus_Y(tmpdir, monkeypatch, annexed, dist_mem):
             output_dm_2 = output_dm_2.replace("annexed", "owned")
         assert output_dm_2 in code
 
+    # Test compilation of generated code
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
 
-def test_int_inc_X_plus_Y(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_X_plus_Y(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncXPlusYKern'
     returns the expected string and 2) we generate correct code for
     the built-in 'X = X + Y' where 'X' and 'Y' are integer-valued
-    fields. Test with and without annexed DoFs being computed as this
-    affects the generated code.
+    fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
-
     _, invoke_info = parse(os.path.join(
         BASE_PATH, "15.21.2_int_inc_X_plus_Y_builtin.f90"), api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_inc_X_plus_Y (increment an "
             "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) + "
-            "f2_data(df)\n"
-            "      END DO\n")
-        assert output in code
-    else:
-        output = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) + "
-            "f2_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()")
-        if not annexed:
-            output = output.replace("dof_annexed", "dof_owned")
-        assert output in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = f1_data(df) + f2_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_a_plus_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_a_plus_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntAPlusXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = a + X' where 'a' is an integer scalar and
-    'X' and 'Y' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    'X' and 'Y' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.21.3_int_a_plus_X_builtin.f90"),
                            api=API)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
 
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule[0].loop_body[0]
     assert str(kern) == "Built-in: int_a_plus_X (integer-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a + f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a + f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = a + f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_a_plus_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_a_plus_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncAPlusXKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = a + X' where 'a' is an integer scalar
-    and 'X' is an integer-valued field. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(
         BASE_PATH, "15.21.4_int_inc_a_plus_X_builtin.f90"), api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_inc_a_plus_X (integer-valued field)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a + f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a + f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = a + f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
 # ------------- Subtracting integer fields ---------------------------------- #
 
 
-def test_int_X_minus_Y(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_X_minus_Y(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntXMinusYKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Z = X - Y' where 'Z', 'X' and 'Y' are
-    integer-valued fields. Test with and without annexed DoFs being
-    computed as this affects the generated code.
+    integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.22.1_int_X_minus_Y_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_X_minus_Y (subtract "
             "integer-valued fields)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      f3_proxy = f3%get_proxy()\n"
-            "      f3_data => f3_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f3\n"
-            "      !\n"
-            "      undf_aspc1_f3 = f3_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f3\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f3_data(df) = f1_data(df) - f2_data(df)\n"
-            "      END DO")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f3_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f3_data(df) = f1_data(df) - f2_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f3_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f3_data(df) = f1_data(df) - f2_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_X_minus_Y(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_X_minus_Y(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncXMinusYKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = X - Y' where 'X' and 'Y' are
-    integer-valued fields. Test with and without annexed DoFs being
-    computed as this affects the generated code.
+    integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(
         os.path.join(BASE_PATH, "15.22.2_int_inc_X_minus_Y_builtin.f90"),
         api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_inc_X_minus_Y (decrement an "
             "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) - f2_data(df)\n"
-            "      END DO\n")
-        assert output in code
-    else:
-        output = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) - f2_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()")
-        if not annexed:
-            output = output.replace("dof_annexed", "dof_owned")
-        assert output in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = f1_data(df) - f2_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_a_minus_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_a_minus_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntAMinusXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = a - X' where 'a' is an integer scalar and
-    'X' and 'Y' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    'X' and 'Y' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.22.3_int_a_minus_X_builtin.f90"),
                            api=API)
 
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule[0].loop_body[0]
     assert str(kern) == "Built-in: int_a_minus_X (integer-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    # Check for the correct 'use' module statements
-    output_mod = (
-        "    USE constants_mod, ONLY: i_def\n"
-        "    USE integer_field_mod, ONLY: integer_field_type,"
-        " integer_field_proxy_type\n")
-    assert output_mod in code
-
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a - f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a - f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = a - f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_a_minus_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_a_minus_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncAMinusXKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = a - X' where 'a' is an integer scalar
-    and 'X' is an integer-valued field. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(
         BASE_PATH, "15.22.4_int_inc_a_minus_X_builtin.f90"), api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_inc_a_minus_X (integer-valued field)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a - f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a - f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = a - f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_X_minus_a(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_X_minus_a(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of LFRicIntXMinusAKern returns
     the expected string and 2) we generate correct code for the
     built-in operation Y = X - a where 'a' is an integer scalar and X
-    and Y are integer-valued fields. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and Y are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.22.5_int_X_minus_a_builtin.f90"),
                            api=API)
 
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule[0].loop_body[0]
     assert str(kern) == "Built-in: int_X_minus_a (integer-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = f1_data(df) - a\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = f1_data(df) - a\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = f1_data(df) - a\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_X_minus_a(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_X_minus_a(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncXMinusAKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = X - a' where 'a' is an integer scalar
-    and 'X' is an integer-valued field. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(
         BASE_PATH, "15.22.6_int_inc_X_minus_a_builtin.f90"), api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_inc_X_minus_a (integer-valued field)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) - a\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) - a\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = f1_data(df) - a\n"
+        "enddo")
+    code = fortran_writer(loop)
+    assert output in code
 
 
 # ------------- Multiplying integer fields ---------------------------------- #
 
 
-def test_int_X_times_Y(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_X_times_Y(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntXTimesYKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Z = X*Y' where 'Z', 'X' and 'Y' are
-    integer-valued fields. Test with and without annexed DoFs being
-    computed as this affects the generated code.
+    integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(
         os.path.join(BASE_PATH,
                      "15.23.1_int_X_times_Y_builtin.f90"), api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_X_times_Y (multiply "
             "integer-valued fields)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "    SUBROUTINE invoke_0(f3, f1, f2)\n"
-            "      TYPE(integer_field_type), intent(in) :: f3, f1, f2\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f3_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f3_proxy, f1_proxy, "
-            "f2_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f3\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f3_proxy = f3%get_proxy()\n"
-            "      f3_data => f3_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f3\n"
-            "      !\n"
-            "      undf_aspc1_f3 = f3_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f3\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f3_data(df) = f1_data(df) * f2_data(df)\n"
-            "      END DO\n")
-        assert output in code
-    else:
-        output = (
-            "      loop0_stop = f3_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f3_data(df) = f1_data(df) * f2_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f3_proxy%set_dirty()")
-        if not annexed:
-            output = output.replace("dof_annexed", "dof_owned")
-        assert output in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f3_data(df) = f1_data(df) * f2_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_X_times_Y(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_X_times_Y(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncXTimesYKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = X*Y' where 'X' and 'Y' are
-    integer-valued fields. Test with and without annexed DoFs being
-    computed as this affects the generated code.
+    integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(
         os.path.join(BASE_PATH,
                      "15.23.2_int_inc_X_times_Y_builtin.f90"),
         api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == ("Built-in: int_inc_X_times_Y (multiply one "
                          "integer-valued field by another)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) * f2_data(df)\n"
-            "      END DO")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = f1_data(df) * f2_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = f1_data(df) * f2_data(df)\n"
+        "enddo")
+    assert output in code
 
 
 # ------------- Scaling integer fields (multiplying by an integer scalar) --- #
 
 
-def test_int_a_times_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_a_times_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntATimesXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = a*X' where 'a' is an integer scalar and
-    'X' and 'Y' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    'X' and 'Y' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(
         os.path.join(BASE_PATH,
                      "15.24.1_int_a_times_X_builtin.f90"),
         api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_a_times_X (copy a scaled "
             "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f2\n"
-            "      !\n"
-            "      undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a_scalar * f1_data(df)\n"
-            "      END DO")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = a_scalar * f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = a_scalar * f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_a_times_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_a_times_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncATimesXKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = a*X' where 'a' is an integer scalar
-    and 'X' is an integer-valued field. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(
         os.path.join(BASE_PATH,
                      "15.24.2_int_inc_a_times_X_builtin.f90"),
         api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_inc_a_times_X (scale an "
             "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "    SUBROUTINE invoke_0(a_scalar, f1)\n"
-            "      INTEGER(KIND=i_def), intent(in) :: a_scalar\n"
-            "      TYPE(integer_field_type), intent(in) :: f1\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f1_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a_scalar * f1_data(df)\n"
-            "      END DO\n"
-            "      !\n")
-        assert output in code
-    else:
-        output_dm = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = a_scalar * f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()")
-        if not annexed:
-            output_dm = output_dm.replace("dof_annexed", "dof_owned")
-        assert output_dm in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = a_scalar * f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
 # ------------- Setting integer field elements to an integer value ---------- #
 
 
-def test_int_setval_c(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_setval_c(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntSetvalCKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'X = c' where 'c' is an integer constant scalar
-    value and 'X' is an integer-valued field. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    value and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.27.1_int_setval_c_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == ("Built-in: int_setval_c (set an integer-valued "
                          "field to a integer scalar value)")
-    # Test code generation
+
+    # Test compilation of generated code
     code = str(psy.gen)
+    assert LFRicBuild(tmpdir).code_compiles(psy)
+
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = c\n"
+        "enddo")
+    assert output in code
+
+
+def test_int_setval_X(tmpdir, fortran_writer):
+    '''Test that 1) the '__str__' method of 'LFRicIntSetvalXKern' returns
+    the expected string and 2) we generate correct code for the
+    built-in operation 'Y = X' where 'X' and 'Y' are integer-valued
+    fields.
+
+    '''
+    _, invoke_info = parse(os.path.join(BASE_PATH,
+                                        "15.27.2_int_setval_X_builtin.f90"),
+                           api=API)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
+    # Test string method
+    first_invoke = psy.invokes.invoke_list[0]
+    kern = first_invoke.schedule.children[0].loop_body[0]
+    assert str(kern) == ("Built-in: int_setval_X (set an integer-valued "
+                         "field equal to another such field)")
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "    SUBROUTINE invoke_0(f1, c)\n"
-            "      INTEGER(KIND=i_def), intent(in) :: c\n"
-            "      TYPE(integer_field_type), intent(in) :: f1\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f1_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f1\n"
-            "      !\n"
-            "      undf_aspc1_f1 = f1_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = c\n"
-            "      END DO")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = c\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
-
-def test_int_setval_X(tmpdir, monkeypatch, annexed, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicIntSetvalXKern' returns
-    the expected string and 2) we generate correct code for the
-    built-in operation 'Y = X' where 'X' and 'Y' are integer-valued
-    fields. Also test with and without annexed DoFs being computed as
-    this affects the generated code.
-
-    '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
-    _, invoke_info = parse(os.path.join(BASE_PATH,
-                                        "15.27.2_int_setval_X_builtin.f90"),
-                           api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
-    # Test string method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == ("Built-in: int_setval_X (set an integer-valued "
-                         "field equal to another such field)")
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
-
-    if not dist_mem:
-        output = (
-            "    SUBROUTINE invoke_0(f2, f1)\n"
-            "      TYPE(integer_field_type), intent(in) :: f2, f1\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f2_proxy, f1_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f2\n"
-            "      !\n"
-            "      undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = f1_data(df)\n"
-            "      END DO")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = f1_data(df)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = f1_data(df)\n"
+        "enddo")
+    assert output in code
 
 
 # ------------- Sign of integer field elements ------------------------------ #
 
 
-def test_int_sign_X(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_sign_X(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntSignXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = sign(a, X)' where 'a' is an integer scalar
-    and 'Y' and 'X' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    and 'Y' and 'X' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.28.1_int_sign_X_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert (str(kern) == "Built-in: int_sign_X (sign of an "
             "integer-valued field)")
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = SIGN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = SIGN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = SIGN(a, f1_data(df))\n"
+        "enddo")
+    assert output in code
 
 
 # ------------- Maximum of (integer scalar, integer field elements) --------- #
 
 
-def test_int_max_aX(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_max_aX(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntMaxAXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = max(a, X)' where 'a' is an integer scalar
-    and 'Y' and 'X' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    and 'Y' and 'X' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.28.3_int_max_aX_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_max_aX (integer-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    # Check for the correct field and scalar type declarations
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
     output = (
-        "      INTEGER(KIND=i_def), intent(in) :: a\n"
-        "      TYPE(integer_field_type), intent(in) :: f2, f1\n"
-        "      INTEGER df\n"
-        "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-        "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-        "null()\n"
-        "      INTEGER(KIND=i_def), pointer, dimension(:) :: f2_data => "
-        "null()\n"
-        "      TYPE(integer_field_proxy_type) f2_proxy, f1_proxy\n")
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = MAX(a, f1_data(df))\n"
+        "enddo")
     assert output in code
 
-    if not dist_mem:
-        assert "INTEGER(KIND=i_def) undf_aspc1_f2\n" in code
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = MAX(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        assert "INTEGER(KIND=i_def) max_halo_depth_mesh\n" in code
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = MAX(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
 
-def test_int_inc_max_aX(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_max_aX(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncMaxAXKern'
     returns the expected string and 2) we generate correct code for
     the built-in operation 'X = max(a, X)' where 'a' is an integer
-    scalar and 'X' is an integer-valued field. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    scalar and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.28.4_int_inc_max_aX_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_inc_max_aX (integer-valued field)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    # Check for the correct field and scalar type declarations
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
     output = (
-        "      INTEGER(KIND=i_def), intent(in) :: a\n"
-        "      TYPE(integer_field_type), intent(in) :: f1\n"
-        "      INTEGER df\n"
-        "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-        "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-        "null()\n"
-        "      TYPE(integer_field_proxy_type) f1_proxy\n")
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = MAX(a, f1_data(df))\n"
+        "enddo")
     assert output in code
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = MAX(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = MAX(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
 
 # ------------- Minimum of (integer scalar, integer field elements) --------- #
 
 
-def test_int_min_aX(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_min_aX(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntMinAXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'Y = min(a, X)' where 'a' is an integer scalar
-    and 'Y' and 'X' are integer-valued fields. Test with and without
-    annexed DoFs being computed as this affects the generated code.
+    and 'Y' and 'X' are integer-valued fields.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.28.5_int_min_aX_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_min_aX (integer-valued fields)"
-    # Test code generation
-    code = str(psy.gen)
 
+    # Test compilation of generated code
+    code = str(psy.gen)
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = MIN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = MIN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
+    output = (
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f2_data(df) = MIN(a, f1_data(df))\n"
+        "enddo")
+    assert output in code
 
 
-def test_int_inc_min_aX(tmpdir, monkeypatch, annexed, dist_mem):
+def test_int_inc_min_aX(tmpdir, fortran_writer):
     '''Test that 1) the '__str__' method of 'LFRicIntIncMinAXKern' returns
     the expected string and 2) we generate correct code for the
     built-in operation 'X = min(a, X)' where 'a' is an integer scalar
-    and 'X' is an integer-valued field. Test with and without annexed
-    DoFs being computed as this affects the generated code.
+    and 'X' is an integer-valued field.
 
     '''
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
     _, invoke_info = parse(os.path.join(BASE_PATH,
                                         "15.28.6_int_inc_min_aX_builtin.f90"),
                            api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
+    psy = PSyFactory(API, distributed_memory=True).create(invoke_info)
+
     # Test string method
     first_invoke = psy.invokes.invoke_list[0]
     kern = first_invoke.schedule.children[0].loop_body[0]
     assert str(kern) == "Built-in: int_inc_min_aX (integer-valued field)"
-    # Test code generation
-    code = str(psy.gen)
-
-    assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    if not dist_mem:
-        output = (
-            "      loop0_stop = undf_aspc1_f1\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = MIN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f1_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f1_data(df) = MIN(a, f1_data(df))\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f1_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
-
-# ------------- Converting integer to real field elements ------------------- #
-
-
-def test_real_X(tmpdir, monkeypatch, annexed, dist_mem):
-    '''Test that 1) the '__str__' method of 'LFRicRealXKern' returns the
-    expected string and 2) we generate correct code for the built-in
-    operation 'Y = real(X, r_def)' where 'Y' is a real-valued field,
-    'X' is the integer-valued field being converted and the correct
-    kind, 'r_def', is read from the PSyclone configuration file. Test
-    with and without annexed DoFs being computed as this affects the
-    generated code. 3) Also test the 'metadata()' method.
-
-    '''
-    metadata = LFRicRealXKern.metadata()
-    assert isinstance(metadata, LFRicKernelMetadata)
-    api_config = Config.get().api_conf(API)
-    monkeypatch.setattr(api_config, "_compute_annexed_dofs", annexed)
-    _, invoke_info = parse(os.path.join(BASE_PATH,
-                                        "15.28.2_real_X_builtin.f90"),
-                           api=API)
-    psy = PSyFactory(API, distributed_memory=dist_mem).create(invoke_info)
-    # Test string method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    assert str(kern) == ("Built-in: real_X (convert an integer-valued to a "
-                         "real-valued field)")
-    # Test code generation
+    # Test compilation of generated code
     code = str(psy.gen)
-
     assert LFRicBuild(tmpdir).code_compiles(psy)
 
-    # First check that the correct field types and constants are used
+    # Test lower_to_language_level() method
+    kern.lower_to_language_level()
+    loop = first_invoke.schedule.walk(Loop)[0]
+    code = fortran_writer(loop)
     output = (
-        "    USE constants_mod, ONLY: r_def, i_def\n"
-        "    USE field_mod, ONLY: field_type, field_proxy_type\n"
-        "    USE integer_field_mod, ONLY: integer_field_type, "
-        "integer_field_proxy_type\n")
+        "do df = loop0_start, loop0_stop, 1\n"
+        "  f1_data(df) = MIN(a, f1_data(df))\n"
+        "enddo")
     assert output in code
-
-    if not dist_mem:
-        output = (
-            "    SUBROUTINE invoke_0(f2, f1)\n"
-            "      TYPE(field_type), intent(in) :: f2\n"
-            "      TYPE(integer_field_type), intent(in) :: f1\n"
-            "      INTEGER df\n"
-            "      INTEGER(KIND=i_def) loop0_start, loop0_stop\n"
-            "      INTEGER(KIND=i_def), pointer, dimension(:) :: f1_data => "
-            "null()\n"
-            "      TYPE(integer_field_proxy_type) f1_proxy\n"
-            "      REAL(KIND=r_def), pointer, dimension(:) :: f2_data => "
-            "null()\n"
-            "      TYPE(field_proxy_type) f2_proxy\n"
-            "      INTEGER(KIND=i_def) undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Initialise field and/or operator proxies\n"
-            "      !\n"
-            "      f2_proxy = f2%get_proxy()\n"
-            "      f2_data => f2_proxy%data\n"
-            "      f1_proxy = f1%get_proxy()\n"
-            "      f1_data => f1_proxy%data\n"
-            "      !\n"
-            "      ! Initialise number of DoFs for aspc1_f2\n"
-            "      !\n"
-            "      undf_aspc1_f2 = f2_proxy%vspace%get_undf()\n"
-            "      !\n"
-            "      ! Set-up all of the loop bounds\n"
-            "      !\n"
-            "      loop0_start = 1\n"
-            "      loop0_stop = undf_aspc1_f2\n"
-            "      !\n"
-            "      ! Call our kernels\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = real(f1_data(df), r_def)\n"
-            "      END DO\n"
-            "      !\n"
-            "    END SUBROUTINE invoke_0\n")
-        assert output in code
-    else:
-        output_dm_2 = (
-            "      loop0_stop = f2_proxy%vspace%get_last_dof_annexed()\n"
-            "      !\n"
-            "      ! Call kernels and communication routines\n"
-            "      !\n"
-            "      DO df=loop0_start,loop0_stop\n"
-            "        f2_data(df) = real(f1_data(df), r_def)\n"
-            "      END DO\n"
-            "      !\n"
-            "      ! Set halos dirty/clean for fields modified in the "
-            "above loop\n"
-            "      !\n"
-            "      CALL f2_proxy%set_dirty()\n"
-            "      !\n")
-        if not annexed:
-            output_dm_2 = output_dm_2.replace("dof_annexed", "dof_owned")
-        assert output_dm_2 in code
-
-
-@pytest.mark.parametrize("kind_name", ["r_solver", "r_tran"])
-def test_real_X_precision(tmpdir, monkeypatch, kind_name):
-    '''Test that the built-in picks up and creates correct code for a
-    scalar with precision that is not the default i.e. not
-    'r_def'. Try with two examples to make sure it works in general.
-
-    '''
-    _, invoke_info = parse(os.path.join(BASE_PATH,
-                                        "15.28.2_real_X_builtin.f90"),
-                           api=API)
-    psy = PSyFactory(API).create(invoke_info)
-    # Test string method
-    first_invoke = psy.invokes.invoke_list[0]
-    kern = first_invoke.schedule.children[0].loop_body[0]
-    monkeypatch.setattr(kern.args[0], "_precision", kind_name)
-    code = str(psy.gen)
-    assert f"USE constants_mod, ONLY: {kind_name}, i_def" in code
-    assert f"f2_data(df) = real(f1_data(df), {kind_name})" in code
-    # Test code generation
-    assert LFRicBuild(tmpdir).code_compiles(psy)
diff --git a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
index cb916e079..f067728a3 100644
--- a/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
+++ b/src/psyclone/tests/domain/lfric/transformations/dynamo0p3_transformations_test.py
@@ -1869,7 +1869,7 @@ def test_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      global_sum%value = asum\n"
@@ -1881,7 +1881,7 @@ def test_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n") in code
 
@@ -1910,7 +1910,7 @@ def test_reduction_real_do(tmpdir, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp end parallel\n"
@@ -1922,7 +1922,7 @@ def test_reduction_real_do(tmpdir, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp end parallel\n") in code
@@ -1956,7 +1956,7 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      global_sum%value = asum\n"
@@ -1969,7 +1969,7 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop1_start,loop1_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      global_sum%value = asum\n"
@@ -1983,7 +1983,7 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      !\n"
@@ -1994,7 +1994,7 @@ def test_multi_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop1_start,loop1_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n") in code
 
@@ -2229,7 +2229,7 @@ def test_two_reductions_real_do(tmpdir, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static), reduction(+:bsum)\n"
@@ -2254,7 +2254,7 @@ def test_two_reductions_real_do(tmpdir, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static), reduction(+:bsum)\n"
@@ -2311,7 +2311,7 @@ def test_two_reprod_reductions_real_do(tmpdir, dist_mem):
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
             "        l_asum(1,th_idx) = l_asum(1,th_idx) + "
-            "f1_data(df)*f2_data(df)\n"
+            "f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -2353,7 +2353,7 @@ def test_two_reprod_reductions_real_do(tmpdir, dist_mem):
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
             "        l_asum(1,th_idx) = l_asum(1,th_idx) + "
-            "f1_data(df)*f2_data(df)\n"
+            "f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -2460,7 +2460,7 @@ def test_multi_different_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      global_sum%value = asum\n"
@@ -2489,7 +2489,7 @@ def test_multi_different_reduction_real_pdo(tmpdir, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      !\n"
@@ -2542,7 +2542,7 @@ def test_multi_builtins_red_then_pdo(tmpdir, monkeypatch, annexed, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      global_sum%value = asum\n"
@@ -2570,7 +2570,7 @@ def test_multi_builtins_red_then_pdo(tmpdir, monkeypatch, annexed, dist_mem):
             "      !$omp parallel do default(shared), private(df), "
             "schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end parallel do\n"
             "      !$omp parallel do default(shared), private(df), "
@@ -2624,7 +2624,7 @@ def test_multi_builtins_red_then_do(tmpdir, monkeypatch, annexed, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -2657,7 +2657,7 @@ def test_multi_builtins_red_then_do(tmpdir, monkeypatch, annexed, dist_mem):
             "      !$omp parallel default(shared), private(df)\n"
             "      !$omp do schedule(static), reduction(+:asum)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        asum = asum + f1_data(df)*f2_data(df)\n"
+            "        asum = asum + f1_data(df) * f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -2714,7 +2714,7 @@ def test_multi_builtins_red_then_fuse_pdo(tmpdir, monkeypatch, annexed,
                 "      !$omp parallel do default(shared), private(df), "
                 "schedule(static), reduction(+:asum)\n"
                 "      DO df=loop0_start,loop0_stop\n"
-                "        asum = asum + f1_data(df)*f2_data(df)\n"
+                "        asum = asum + f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end parallel do\n"
@@ -2738,7 +2738,7 @@ def test_multi_builtins_red_then_fuse_pdo(tmpdir, monkeypatch, annexed,
                 "      !$omp parallel do default(shared), private(df), "
                 "schedule(static), reduction(+:asum)\n"
                 "      DO df=loop0_start,loop0_stop\n"
-                "        asum = asum + f1_data(df)*f2_data(df)\n"
+                "        asum = asum + f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end parallel do\n")
@@ -2792,7 +2792,7 @@ def test_multi_builtins_red_then_fuse_do(tmpdir, monkeypatch, annexed,
                 "      !$omp parallel default(shared), private(df)\n"
                 "      !$omp do schedule(static), reduction(+:asum)\n"
                 "      DO df=loop0_start,loop0_stop\n"
-                "        asum = asum + f1_data(df)*f2_data(df)\n"
+                "        asum = asum + f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end do\n"
@@ -2815,7 +2815,7 @@ def test_multi_builtins_red_then_fuse_do(tmpdir, monkeypatch, annexed,
                 "      !$omp parallel default(shared), private(df)\n"
                 "      !$omp do schedule(static), reduction(+:asum)\n"
                 "      DO df=loop0_start,loop0_stop\n"
-                "        asum = asum + f1_data(df)*f2_data(df)\n"
+                "        asum = asum + f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end do\n"
@@ -3139,8 +3139,8 @@ def test_reprod_reduction_real_do(tmpdir, dist_mem):
             "      th_idx = omp_get_thread_num()+1\n"
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df)"
-            "*f2_data(df)\n"
+            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df) "
+            "* f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp end parallel\n"
@@ -3164,8 +3164,8 @@ def test_reprod_reduction_real_do(tmpdir, dist_mem):
             "      th_idx = omp_get_thread_num()+1\n"
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df)"
-            "*f2_data(df)\n"
+            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df) "
+            "* f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp end parallel\n"
@@ -3258,8 +3258,8 @@ def test_reprod_builtins_red_then_usual_do(tmpdir, monkeypatch, annexed,
             "      th_idx = omp_get_thread_num()+1\n"
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df)"
-            "*f2_data(df)\n"
+            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df) "
+            "* f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -3298,8 +3298,8 @@ def test_reprod_builtins_red_then_usual_do(tmpdir, monkeypatch, annexed,
             "      th_idx = omp_get_thread_num()+1\n"
             "      !$omp do schedule(static)\n"
             "      DO df=loop0_start,loop0_stop\n"
-            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df)"
-            "*f2_data(df)\n"
+            "        l_asum(1,th_idx) = l_asum(1,th_idx) + f1_data(df) "
+            "* f2_data(df)\n"
             "      END DO\n"
             "      !$omp end do\n"
             "      !$omp do schedule(static)\n"
@@ -3380,7 +3380,7 @@ def test_repr_bltins_red_then_usual_fuse_do(tmpdir, monkeypatch, annexed,
                 "      !$omp do schedule(static)\n"
                 "      DO df=loop0_start,loop0_stop\n"
                 "        l_asum(1,th_idx) = l_asum(1,th_idx) + "
-                "f1_data(df)*f2_data(df)\n"
+                "f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end do\n"
@@ -3414,7 +3414,7 @@ def test_repr_bltins_red_then_usual_fuse_do(tmpdir, monkeypatch, annexed,
                 "      !$omp do schedule(static)\n"
                 "      DO df=loop0_start,loop0_stop\n"
                 "        l_asum(1,th_idx) = l_asum(1,th_idx) + "
-                "f1_data(df)*f2_data(df)\n"
+                "f1_data(df) * f2_data(df)\n"
                 "        f1_data(df) = bsum * f1_data(df)\n"
                 "      END DO\n"
                 "      !$omp end do\n"
@@ -3551,7 +3551,7 @@ def test_repr_3_builtins_2_reductions_do(tmpdir, dist_mem):
 
         for names in [
                 {"var": "asum", "lvar": "l_asum", "loop_idx": "0",
-                 "rhs": "f1_data(df)*f2_data(df)"},
+                 "rhs": "f1_data(df) * f2_data(df)"},
                 {"var": "bsum", "lvar": "l_bsum", "loop_idx": "2",
                  "rhs": "f2_data(df)"}]:
             assert (
@@ -3587,7 +3587,7 @@ def test_repr_3_builtins_2_reductions_do(tmpdir, dist_mem):
 
         for names in [
                 {"var": "asum", "lvar": "l_asum", "loop_idx": "0",
-                 "rhs": "f1_data(df)*f2_data(df)"},
+                 "rhs": "f1_data(df) * f2_data(df)"},
                 {"var": "bsum", "lvar": "l_bsum",
                  "loop_idx": "2", "rhs": "f2_data(df)"}]:
             assert (
@@ -4222,7 +4222,7 @@ def test_rc_all_discontinuous_vector_depth(tmpdir):
     loop = schedule.children[0]
     rc_trans.apply(loop, {"depth": 3})
     result = str(psy.gen)
-    print(result)
+
     for idx in range(1, 4):
         assert f"IF (f2_proxy({idx})%is_dirty(depth=3)) THEN" in result
         assert f"CALL f2_proxy({idx})%halo_exchange(depth=3)" in result
diff --git a/src/psyclone/tests/dynamo0p3_stubgen_test.py b/src/psyclone/tests/dynamo0p3_stubgen_test.py
index adb9b8c76..6297ff4fe 100644
--- a/src/psyclone/tests/dynamo0p3_stubgen_test.py
+++ b/src/psyclone/tests/dynamo0p3_stubgen_test.py
@@ -165,7 +165,8 @@ def test_load_meta_wrong_type():
     with pytest.raises(GenerationError) as excinfo:
         kernel.load_meta(metadata)
     const = LFRicConstants()
-    assert (f"LFRicKern.load_meta() expected one of {const.VALID_ARG_TYPE_NAMES}"
+    assert (f"LFRicKern.load_meta() expected one of "
+            f"{const.VALID_ARG_TYPE_NAMES}"
             f" but found 'gh_hedge'" in str(excinfo.value))