Repository: stfc/PSyclone

Issue:
Issues with InliningTrans (with ArraryRef to StructureofArrayRef conversion?)
Trying to inline the call in:
```
     DO j = ssha_t%internal%ystart, ssha_t%internal%ystop, 1
        DO i = ssha_t%internal%xstart, ssha_t%internal%xstop, 1
          CALL continuity_code(i, j, ssha_t%data, sshn_t%data, sshn_u%data, sshn_v%data, hu%data, hv%data, un%data, vn%data, &
&sshn_t%grid%area_t, rdt)
        END DO
      END DO
```
which references the subroutine:
```
SUBROUTINE continuity_code(ji, jj, ssha, sshn, sshn_u, sshn_v, hu, hv, un, vn, e12t, rdt)
  ...
  rtmp1 = (sshn_u(ji,jj) + hu(ji,jj)) * un(ji,jj)
  ...
```

Ends up with:
```
      DO j = ssha_t%internal%ystart, ssha_t%internal%ystop, 1
        DO i = ssha_t%internal%xstart, ssha_t%internal%xstop, 1
          rtmp1 = (sshn_u%data + hu%data) * un%data
          rtmp2 = (sshn_u%data + hu%data) * un%data
          rtmp3 = (sshn_v%data + hv%data) * vn%data
          rtmp4 = (sshn_v%data + hv%data) * vn%data
          ssha_t%data = sshn_t%data + (rtmp2 - rtmp1 + rtmp4 - rtmp3) * rdt / sshn_t%grid%area_t
        END DO
      END DO
```
which misses the array access expressions.

Discussion:

Patch:
diff --git a/changelog b/changelog
index 23a14a7cf..5d56aef13 100644
--- a/changelog
+++ b/changelog
@@ -318,6 +318,9 @@
 	107) PR #2032 for #92. Forbid include statements (which should have been
 	processed by the preprocessor or fparser before PSyclone).
 
+	108) PR #1973 for #1904. Fix inlining issues with complex access
+	expressions (e.g. array offsets and partial derived types accesses).
+
 release 2.3.1 17th of June 2022
 
 	1) PR #1747 for #1720. Adds support for If blocks to PSyAD.
diff --git a/doc/developer_guide/transformations.rst b/doc/developer_guide/transformations.rst
index fdb2cf1d5..97c81d845 100644
--- a/doc/developer_guide/transformations.rst
+++ b/doc/developer_guide/transformations.rst
@@ -1,7 +1,7 @@
 .. -----------------------------------------------------------------------------
 .. BSD 3-Clause License
 ..
-.. Copyright (c) 2019-2022, Science and Technology Facilities Council.
+.. Copyright (c) 2019-2023, Science and Technology Facilities Council.
 .. All rights reserved.
 ..
 .. Redistribution and use in source and binary forms, with or without
@@ -357,6 +357,65 @@ issues:
    subject of #685. For the moment the test just checks for MATMUL as
    that is currently the only non-elementwise operation in the PSyiR.
 
+Inlining
+========
+
+PSyclone supports two different inlining transformations:
+``KernelModuleInlineTrans`` and ``InlineTrans``. The former is relatively
+simple and creates a copy of the Kernel routine within the same Container
+as the routine from which it is called. The latter is far more intrusive
+and replaces a call to a routine with the actual body of that routine.
+This can be complex due to the fact that Fortran allows the bounds of
+arrays within a routine to differ from those at the call site, e.g.:
+
+.. code-block:: fortran
+
+  integer :: my_array(10)
+  ...
+  call my_sub(my_array)
+
+  subroutine my_sub(x)
+    integer, intent(inout), :: x(2:11)
+    ...
+
+As a consequence of this, ensuring that any array index expressions are
+correctly handled when inlining the routine body will often mean that
+full type information is required for every dummy argument. However, there are
+exceptions that arise when the dimensions of an array are not actually
+declared within the subroutine, e.g.:
+
+.. code-block:: fortran
+
+   type(my_type) :: var
+   ...
+   call my_sub(var)
+
+   subroutine my_sub(x)
+     type(my_var), intent(inout) :: x
+     x%data(3:5) = ...
+
+In this case, the definition of the array being accessed is contained
+within `my_var` and is therefore common to both the call site and
+the subroutine. We therefore do not require full type information
+for the dummy argument `x` in order to safely inline the routine.
+However, as soon as the dummy argument is in the form of an array
+then we will require full type information for the corresponding
+argument at both the call site and within the routine, e.g.:
+
+.. code-block:: fortran
+
+   type(my_type) :: var
+   ...
+   call my_sub(var%data)
+
+   subroutine my_sub(x)
+     real, dimension(2:5, 6) :: x
+     x(2, 4) = ...
+
+In this case, the correct index expressions for the inlined code will
+depend on the bounds with which the `data` member of `my_type` is
+declared and therefore this information must be available.
+
 OpenMP Tasking
 ==============
 OpenMP tasking is supported in PSyclone, currently by the combination
diff --git a/src/psyclone/psyir/nodes/array_mixin.py b/src/psyclone/psyir/nodes/array_mixin.py
index 794055f89..1455988fb 100644
--- a/src/psyclone/psyir/nodes/array_mixin.py
+++ b/src/psyclone/psyir/nodes/array_mixin.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2021-2022, Science and Technology Facilities Council.
+# Copyright (c) 2021-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -49,9 +49,9 @@ from psyclone.psyir.nodes.member import Member
 from psyclone.psyir.nodes.operation import Operation, BinaryOperation
 from psyclone.psyir.nodes.ranges import Range
 from psyclone.psyir.nodes.reference import Reference
-from psyclone.psyir.symbols import SymbolError, DataSymbol
-from psyclone.psyir.symbols.datatypes import (ScalarType, ArrayType,
-                                              INTEGER_TYPE)
+from psyclone.psyir.symbols import SymbolError, DataSymbol, DataTypeSymbol
+from psyclone.psyir.symbols.datatypes import (
+    ScalarType, ArrayType, DeferredType, UnknownType, INTEGER_TYPE)
 
 
 class ArrayMixin(metaclass=abc.ABCMeta):
@@ -188,6 +188,80 @@ class ArrayMixin(metaclass=abc.ABCMeta):
             return False
         return True
 
+    def get_lbound_expression(self, pos):
+        '''
+        Lookup the lower bound of this ArrayMixin. If we don't have the
+        necessary type information then a call to the LBOUND intrinsic is
+        constructed and returned.
+
+        :param int pos: the dimension of the array for which to lookup the \
+                        lower bound.
+
+        :returns: the declared lower bound for the specified dimension of \
+            the array accesed or a call to the LBOUND intrinsic if it is \
+            unknown.
+        :rtype: :py:class:`psyclone.psyir.nodes.Node`
+
+        '''
+        # First, walk up to the parent reference and get its type. For a simple
+        # ArrayReference this will just be self.
+        root_ref = self.ancestor(Reference, include_self=True)
+        cursor_type = root_ref.symbol.datatype
+
+        # Walk back down the structure, looking up the type information as we
+        # go. We also collect the necessary information for creating a new
+        # Reference as argument to the LBOUND intrinsic in case the type
+        # information is not available.
+        cnames = []
+        cursor = root_ref
+        while cursor is not self:
+            cursor = cursor.member
+            # Collect member information.
+            if isinstance(cursor, ArrayMixin):
+                new_indices = [idx.copy() for idx in cursor.indices]
+                cnames.append((cursor.name, new_indices))
+            else:
+                cnames.append(cursor.name)
+            # Continue to resolve datatype unless we hit an
+            # UnknownType or DeferredType.
+            if isinstance(cursor_type, ArrayType):
+                cursor_type = cursor_type.intrinsic
+            if isinstance(cursor_type, DataTypeSymbol):
+                cursor_type = cursor_type.datatype
+            if isinstance(cursor_type, (UnknownType, DeferredType)):
+                continue
+            cursor_type = cursor_type.components[cursor.name].datatype
+
+        if (isinstance(cursor_type, ArrayType) and
+                cursor_type.shape[pos] not in [ArrayType.Extent.DEFERRED,
+                                               ArrayType.Extent.ATTRIBUTE]):
+            # We have the full type information and the lower bound is known.
+            return cursor_type.shape[pos].lower.copy()
+
+        # We've either failed to resolve the type or we don't know the extent
+        # of the array dimension so construct a call to the LBOUND intrinsic.
+        if cnames:
+            # We have some sort of structure access - remove any indexing
+            # information from the ultimate member of the structure access.
+            if len(cnames[-1]) == 2:
+                cnames[-1] = cnames[-1][0]
+            # Have to import here to avoid circular dependencies.
+            # pylint: disable=import-outside-toplevel
+            from psyclone.psyir.nodes import (ArrayOfStructuresReference,
+                                              StructureReference)
+            if isinstance(root_ref, ArrayMixin):
+                new_indices = [idx.copy() for idx in root_ref.indices]
+                ref = ArrayOfStructuresReference.create(
+                    root_ref.symbol, new_indices, cnames)
+            else:
+                ref = StructureReference.create(root_ref.symbol, cnames)
+        else:
+            # A simple Reference.
+            ref = Reference(root_ref.symbol)
+
+        return BinaryOperation.create(BinaryOperation.Operator.LBOUND, ref,
+                                      Literal(str(pos+1), INTEGER_TYPE))
+
     def is_upper_bound(self, index):
         '''Returns True if the specified array index contains a Range node
         which has a stopping value given by the 'UBOUND(name,index)'
@@ -418,15 +492,16 @@ class ArrayMixin(metaclass=abc.ABCMeta):
 
             elif isinstance(idx_expr, Reference):
                 dtype = idx_expr.datatype
-                if dtype.shape:
+                if isinstance(dtype, ArrayType):
                     # An array slice can be defined by a 1D slice of another
                     # array, e.g. `a(b(1:4))`.
-                    if len(dtype.shape) > 1:
+                    indirect_array_shape = dtype.shape
+                    if len(indirect_array_shape) > 1:
                         raise InternalError(
                             f"An array defining a slice of a dimension of "
                             f"another array must be 1D but '{idx_expr.name}' "
                             f"used to index into '{self.name}' has "
-                            f"{len(dtype.shape)} dimensions.")
+                            f"{len(indirect_array_shape)} dimensions.")
                     shape.append(_num_elements(dtype.shape[0]))
             elif isinstance(idx_expr, (Call, Operation, CodeBlock)):
                 # We can't yet straightforwardly query the type of a function
diff --git a/src/psyclone/psyir/nodes/array_reference.py b/src/psyclone/psyir/nodes/array_reference.py
index af2114a81..7329b1e29 100644
--- a/src/psyclone/psyir/nodes/array_reference.py
+++ b/src/psyclone/psyir/nodes/array_reference.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2022, Science and Technology Facilities Council.
+# Copyright (c) 2020-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -45,7 +45,8 @@ from psyclone.psyir.nodes.operation import BinaryOperation
 from psyclone.psyir.nodes.ranges import Range
 from psyclone.psyir.nodes.reference import Reference
 from psyclone.psyir.symbols import (DataSymbol, DeferredType, UnknownType,
-                                    ScalarType, ArrayType, INTEGER_TYPE)
+                                    DataTypeSymbol, ScalarType, ArrayType,
+                                    INTEGER_TYPE)
 
 
 class ArrayReference(ArrayMixin, Reference):
@@ -128,6 +129,8 @@ class ArrayReference(ArrayMixin, Reference):
         shape = self._get_effective_shape()
         if shape:
             return ArrayType(self.symbol.datatype, shape)
+        if isinstance(self.symbol.datatype.intrinsic, DataTypeSymbol):
+            return self.symbol.datatype.intrinsic
         # TODO #1857: Really we should just be able to return
         # self.symbol.datatype here but currently arrays of scalars are
         # handled in a different way to all other types of array.
diff --git a/src/psyclone/psyir/nodes/member.py b/src/psyclone/psyir/nodes/member.py
index 9c2177f8b..3adbb3596 100644
--- a/src/psyclone/psyir/nodes/member.py
+++ b/src/psyclone/psyir/nodes/member.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2022, Science and Technology Facilities Council.
+# Copyright (c) 2020-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -38,7 +38,6 @@
 
 ''' This module contains the implementation of the Member node.'''
 
-from __future__ import absolute_import
 
 from psyclone.core import Signature
 from psyclone.psyir.nodes.node import Node
@@ -79,7 +78,7 @@ class Member(Node):
                 f"(ArrayOf)Structure(s)Reference or (ArrayOf)Structure(s)"
                 f"Member but found '{type(parent).__name__}'.")
 
-        super(Member, self).__init__(parent=parent)
+        super().__init__(parent=parent)
         # Store the name of the component that this member represents
         self._component_name = member_name
 
@@ -121,7 +120,7 @@ class Member(Node):
 
     @property
     def is_array(self):
-        ''':returns: if this member is an array.
+        ''':returns: whether this member is an array.
         :rtype: bool
 
         '''
diff --git a/src/psyclone/psyir/nodes/reference.py b/src/psyclone/psyir/nodes/reference.py
index 811125f01..7aa9a4109 100644
--- a/src/psyclone/psyir/nodes/reference.py
+++ b/src/psyclone/psyir/nodes/reference.py
@@ -43,6 +43,7 @@ from psyclone.core import AccessType, Signature
 # We cannot import from 'nodes' directly due to circular import
 from psyclone.psyir.nodes.datanode import DataNode
 from psyclone.psyir.symbols import Symbol
+from psyclone.psyir.symbols.datatypes import DeferredType
 
 
 class Reference(DataNode):
@@ -172,6 +173,9 @@ class Reference(DataNode):
         :returns: the datatype of this reference.
         :rtype: :py:class:`psyclone.psyir.symbols.DataType`
         '''
+        if type(self.symbol) is Symbol:
+            # We don't even have a DataSymbol
+            return DeferredType()
         return self.symbol.datatype
 
 
diff --git a/src/psyclone/psyir/nodes/structure_reference.py b/src/psyclone/psyir/nodes/structure_reference.py
index 50c17ff15..bc7a5144e 100644
--- a/src/psyclone/psyir/nodes/structure_reference.py
+++ b/src/psyclone/psyir/nodes/structure_reference.py
@@ -42,8 +42,8 @@ from psyclone.psyir.nodes.reference import Reference
 from psyclone.psyir.nodes.member import Member
 from psyclone.psyir.nodes.array_member import ArrayMember
 from psyclone.psyir.nodes.array_mixin import ArrayMixin
-from psyclone.psyir.nodes.array_of_structures_member import \
-    ArrayOfStructuresMember
+from psyclone.psyir.nodes.array_of_structures_member import (
+    ArrayOfStructuresMember)
 from psyclone.psyir.nodes.structure_member import StructureMember
 from psyclone.psyir.symbols import (ArrayType, DataSymbol, DataType,
                                     DataTypeSymbol, DeferredType, ScalarType,
@@ -319,8 +319,12 @@ class StructureReference(Reference):
 
         # Walk down the structure, collecting information on any array slices
         # as we go.
-        while hasattr(cursor, "member"):
+        while isinstance(cursor, (StructureMember, StructureReference)):
             cursor = cursor.member
+            if isinstance(cursor_type, ArrayType):
+                cursor_type = cursor_type.intrinsic
+            if isinstance(cursor_type, DataTypeSymbol):
+                cursor_type = cursor_type.datatype
             cursor_type = cursor_type.components[cursor.name].datatype
             if isinstance(cursor_type, (UnknownType, DeferredType)):
                 return DeferredType()
@@ -356,7 +360,6 @@ class StructureReference(Reference):
                         f"'{cursor.name}' of the StructureAccess represents "
                         f"an array but other array notation is present in the "
                         f"full access expression: '{fwriter(self)}'")
-                return ArrayType(cursor_type.intrinsic, shape)
 
             return ArrayType(cursor_type, shape)
 
diff --git a/src/psyclone/psyir/symbols/symbol_table.py b/src/psyclone/psyir/symbols/symbol_table.py
index 24c122627..2db5cb4fe 100644
--- a/src/psyclone/psyir/symbols/symbol_table.py
+++ b/src/psyclone/psyir/symbols/symbol_table.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2017-2022, Science and Technology Facilities Council.
+# Copyright (c) 2017-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -40,16 +40,15 @@
 
 # pylint: disable=too-many-lines
 
-from __future__ import print_function, absolute_import
 from collections import OrderedDict
 import inspect
 import copy
 from psyclone.configuration import Config
+from psyclone.errors import InternalError
 from psyclone.psyir.symbols import Symbol, DataSymbol, ImportInterface, \
     ContainerSymbol, DataTypeSymbol, RoutineSymbol, SymbolError, \
     UnresolvedInterface
 from psyclone.psyir.symbols.typed_symbol import TypedSymbol
-from psyclone.errors import InternalError
 
 
 class SymbolTable():
@@ -1248,7 +1247,7 @@ class SymbolTable():
                                 symbol_match.specialise(type(symbol))
 
                         symbol_match.copy_properties(symbol)
-                        # Restore the interface and visibility as this are
+                        # Restore the interface and visibility as these are
                         # local (not imported) properties
                         symbol_match.interface = interface
                         symbol_match.visibility = visibility
@@ -1264,6 +1263,10 @@ class SymbolTable():
                         new_symbol.interface = ImportInterface(c_symbol)
                         new_symbol.visibility = self.default_visibility
                         self.add(new_symbol)
+                        if symbol_target:
+                            # If we were looking just for this symbol then
+                            # we're done.
+                            return
 
         if symbol_target:
             raise KeyError(
diff --git a/src/psyclone/psyir/transformations/inline_trans.py b/src/psyclone/psyir/transformations/inline_trans.py
index e4e6e0fde..5ba9406d9 100644
--- a/src/psyclone/psyir/transformations/inline_trans.py
+++ b/src/psyclone/psyir/transformations/inline_trans.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2022, Science and Technology Facilities Council.
+# Copyright (c) 2022-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -31,8 +31,7 @@
 # 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: R. W. Ford, STFC Daresbury Lab
+# Authors: A. R. Porter, R. W. Ford and A. Chalk, STFC Daresbury Lab
 
 '''
 This module contains the InlineTrans transformation.
@@ -42,14 +41,23 @@ from psyclone.errors import InternalError, LazyString
 from psyclone.psyGen import Transformation
 from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.psyir.nodes import (
-    ArrayReference, Call, Range, Routine, Reference, CodeBlock,
-    Return, Literal, Assignment, Container)
+    ArrayReference, ArrayOfStructuresReference, BinaryOperation, Call,
+    CodeBlock, Container, IntrinsicCall, Range, Routine, Reference, Return,
+    Literal, Assignment, StructureMember, StructureReference)
+from psyclone.psyir.nodes.array_mixin import ArrayMixin
 from psyclone.psyir.symbols import (ContainerSymbol, DataSymbol, ScalarType,
-                                    ImportInterface, Symbol)
+                                    RoutineSymbol, ImportInterface, Symbol,
+                                    ArrayType, INTEGER_TYPE, DeferredType,
+                                    UnknownType)
+from psyclone.psyir.transformations.reference2arrayrange_trans import (
+    Reference2ArrayRangeTrans)
 from psyclone.psyir.transformations.transformation_error import (
     TransformationError)
 
 
+_ONE = Literal("1", INTEGER_TYPE)
+
+
 class InlineTrans(Transformation):
     '''
     This transformation takes a Call (which may have a return value)
@@ -101,8 +109,8 @@ class InlineTrans(Transformation):
 
         * the routine is not in the same file as the call;
         * the routine contains an early Return statement;
+        * the routine contains a variable with UnknownType;
         * the routine has a named argument;
-        * the call to the routine passes array subsections;
         * the shape of any array arguments as declared inside the routine does
           not match the shape of the arrays being passed as arguments;
         * the routine accesses an un-resolved symbol;
@@ -125,7 +133,7 @@ class InlineTrans(Transformation):
         '''
         self.validate(node, options)
 
-        # The table we will copy symbols into.
+        # The table associated with the scoping region holding the Call.
         table = node.scope.symbol_table
         # Find the routine to be inlined.
         orig_routine = self._find_routine(node)
@@ -161,17 +169,29 @@ class InlineTrans(Transformation):
         self._inline_container_symbols(table, routine_table)
 
         # Copy each Symbol from the Routine into the symbol table associated
-        # with the call site, excluding those that represent dummy arguments
+        # with the call site, excluding those that represent formal arguments
         # or containers.
         self._inline_symbols(table, routine_table, precision_map)
 
-        # Replace any references to dummy arguments with copies of the
+        # When constructing new references to replace references to formal
+        # args, we need to know whether any of the actual arguments are array
+        # accesses. If they use 'array notation' (i.e. represent a whole array)
+        # then they won't have index expressions and will have been captured
+        # as a Reference.
+        ref2arraytrans = Reference2ArrayRangeTrans()
+
+        for child in node.children:
+            try:
+                # TODO #1858, this won't yet work for arrays inside structures.
+                ref2arraytrans.apply(child)
+            except (TransformationError, ValueError):
+                pass
+
+        # Replace any references to formal arguments with copies of the
         # actual arguments.
-        dummy_args = routine_table.argument_list
-        for ref in refs:
-            if ref.symbol in dummy_args:
-                ref.replace_with(
-                    node.children[dummy_args.index(ref.symbol)].copy())
+        formal_args = routine_table.argument_list
+        for ref in refs[:]:
+            self._replace_formal_arg(ref, node, formal_args)
 
         # Copy the nodes from the Routine into the call site.
         if isinstance(new_stmts[-1], Return):
@@ -202,6 +222,345 @@ class InlineTrans(Transformation):
                 idx += 1
                 parent.addchild(child, idx)
 
+    def _replace_formal_arg(self, ref, call_node, formal_args):
+        '''
+        Recursively combines any References to formal arguments in the supplied
+        PSyIR expression with the corresponding Reference (actual argument)
+        from the call site to make a new Reference for use in the inlined code.
+        If the supplied node is not a Reference to a formal argument then it is
+        just returned (after we have recursed to any children).
+
+        :param ref: the expression to update.
+        :type ref: :py:class:`psyclone.psyir.nodes.Node`
+        :param call_node: the call site.
+        :type call_node: :py:class:`psyclone.psyir.nodes.Call`
+        :param formal_args: the formal arguments of the called routine.
+        :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
+
+        :returns: the replacement reference.
+        :rtype: :py:class:`psyclone.psyir.nodes.Reference`
+
+        '''
+        if not isinstance(ref, Reference):
+            # Recurse down in case this is e.g. an Operation or Range.
+            for child in ref.children[:]:
+                self._replace_formal_arg(child, call_node, formal_args)
+            return ref
+
+        if ref.symbol not in formal_args:
+            # The supplied reference is not to a formal argument.
+            return ref
+
+        # Lookup the actual argument that corresponds to this formal argument.
+        actual_arg = call_node.children[formal_args.index(ref.symbol)]
+
+        # If the local reference is a simple Reference then we can just
+        # replace it with a copy of the actual argument, e.g.
+        #
+        #   call my_sub(my_struc%data(i,j))
+        #
+        #   subroutine my_sub(var)
+        #     ...
+        #     var = 0.0
+        #
+        # pylint: disable=unidiomatic-typecheck
+        if type(ref) is Reference:
+            arg_copy = actual_arg.copy()
+            # If the local reference we are replacing has a parent then we
+            # must ensure the parent's child list is updated. (It may not
+            # have a parent if we are in the process of constructing a brand
+            # new reference.)
+            if ref.parent:
+                ref.replace_with(arg_copy)
+            return arg_copy
+
+        # Local reference is not simple but the actual argument is, e.g.:
+        #
+        #   call my_sub(my_struc)
+        #
+        #   subroutine my_sub(var)
+        #     ...
+        #     var%data(i,j) = 0.0
+        #
+        if type(actual_arg) is Reference:
+            ref.symbol = actual_arg.symbol
+            return ref
+
+        # Neither the actual or local references are simple, i.e. they
+        # include array accesses and/or structure accesses.
+        new_ref = self._replace_formal_struc_arg(actual_arg, ref, call_node,
+                                                 formal_args)
+        # If the local reference we are replacing has a parent then we must
+        # ensure the parent's child list is updated. (It may not have a parent
+        # if we are in the process of constructing a brand new reference.)
+        if ref.parent:
+            ref.replace_with(new_ref)
+        return new_ref
+
+    def _create_inlined_idx(self, call_node, formal_args,
+                            local_idx, decln_start, actual_start):
+        '''
+        Utility that creates the PSyIR for an inlined array-index access
+        expression. This is not trivial since a formal argument may be
+        declared with bounds that are shifted relative to those of an
+        actual argument.
+
+        If local_idx is the index of the access in the routine;
+           local_decln_start is the starting index of the dimension as
+                        declared in the routine;
+           actual_start is the starting index of the slice at the callsite
+                        (whether from the array declaration or a slice);
+
+        then the index of the inlined access will be::
+
+            inlined_idx = local_idx - local_decln_start + 1 + actual_start - 1
+                        = local_idx - local_decln_start + actual_start
+
+        :param call_node: the Call that we are inlining.
+        :type call_node: :py:class:`psyclone.psyir.nodes.Call`
+        :param formal_args: the formal arguments of the routine being called.
+        :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
+        :param local_idx: a local array-index expression (i.e. appearing \
+            within the routine being inlined).
+        :type local_idx: :py:class:`psyclone.psyir.nodes.Node`
+        :param decln_start: the lower bound of the corresponding array \
+            dimension, as declared inside the routine being inlined.
+        :type decln_start: :py:class:`psyclone.psyir.nodes.Node`
+        :param actual_start: the lower bound of the corresponding array \
+            dimension, as defined at the call site.
+        :type actual_start: :py:class:`psyclone.psyir.nodes.Node`
+
+        :returns: PSyIR for the corresponding inlined array index.
+        :rtype: :py:class:`psyclone.psyir.nodes.Node`
+
+        '''
+        if isinstance(local_idx, Range):
+            lower = self._create_inlined_idx(call_node, formal_args,
+                                             local_idx.start, decln_start,
+                                             actual_start)
+            upper = self._create_inlined_idx(call_node, formal_args,
+                                             local_idx.stop, decln_start,
+                                             actual_start)
+            step = self._replace_formal_arg(local_idx.step, call_node,
+                                            formal_args)
+            return Range.create(lower.copy(), upper.copy(), step.copy())
+
+        uidx = self._replace_formal_arg(local_idx, call_node, formal_args)
+        if decln_start == actual_start:
+            # If the starting indices in the actual and formal arguments are
+            # the same then we don't need to shift the index.
+            return uidx
+
+        ustart = self._replace_formal_arg(decln_start, call_node, formal_args)
+        start_sub = BinaryOperation.create(BinaryOperation.Operator.SUB,
+                                           uidx.copy(), ustart.copy())
+        return BinaryOperation.create(BinaryOperation.Operator.ADD,
+                                      start_sub, actual_start.copy())
+
+    def _update_actual_indices(self, actual_arg, local_ref,
+                               call_node, formal_args):
+        '''
+        Create a new list of indices for the supplied actual argument
+        (ArrayMixin) by replacing any Ranges with the appropriate expressions
+        from the local access in the called routine. If there are no Ranges
+        then the returned list of indices just contains copies of the inputs.
+
+        :param actual_arg: (part of) the actual argument to the routine.
+        :type actual_arg: :py:class:`psyclone.psyir.nodes.ArrayMixin`
+        :param local_ref: the corresponding Reference in the called routine.
+        :param call_node: the call site.
+        :type call_node: :py:class:`psyclone.psyir.nodes.Call`
+        :param formal_args: the formal arguments of the called routine.
+        :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
+
+        :returns: new indices for the actual argument.
+        :rtype: List[:py:class:`psyclone.psyir.nodes.Node`]
+
+        '''
+        if isinstance(local_ref, ArrayMixin):
+            local_indices = [idx.copy() for idx in local_ref.indices]
+        # Get the locally-declared shape of the formal argument in case its
+        # bounds are shifted relative to the caller.
+        if isinstance(local_ref.symbol.datatype, ArrayType):
+            local_decln_shape = local_ref.symbol.datatype.shape
+        else:
+            local_decln_shape = []
+
+        new_indices = [idx.copy() for idx in actual_arg.indices]
+        local_idx_posn = 0
+        for pos, idx in enumerate(new_indices[:]):
+
+            if not isinstance(idx, Range):
+                continue
+
+            # Starting index of slice of actual argument.
+            if actual_arg.is_lower_bound(pos):
+                # Range starts at lower bound of argument so that's what
+                # we store.
+                actual_start = actual_arg.get_lbound_expression(pos)
+            else:
+                actual_start = idx.start
+
+            local_decln_start = None
+            if local_decln_shape:
+                if isinstance(local_decln_shape[local_idx_posn],
+                              ArrayType.ArrayBounds):
+                    # The formal argument declaration has a shape.
+                    local_shape = local_decln_shape[local_idx_posn]
+                    local_decln_start = local_shape.lower
+                elif (local_decln_shape[local_idx_posn] ==
+                      ArrayType.Extent.DEFERRED):
+                    # The formal argument is declared to be allocatable and
+                    # therefore has the same bounds as the actual argument.
+                    local_shape = None
+                    local_decln_start = actual_start
+            if not local_decln_start:
+                local_shape = None
+                local_decln_start = _ONE
+
+            if local_ref.is_full_range(local_idx_posn):
+                # If the local Range is for the full extent of the formal
+                # argument then the actual Range is defined by that of the
+                # actual argument and no change is required unless the formal
+                # argument is declared as having a Range with an extent that is
+                # less than that supplied. In general we're not going to know
+                # that so we have to be conservative.
+                if local_shape:
+                    new = Range.create(local_shape.lower.copy(),
+                                       local_shape.upper.copy())
+                    new_indices[pos] = self._create_inlined_idx(
+                        call_node, formal_args,
+                        new, local_decln_start, actual_start)
+            else:
+                # Otherwise, the local index expression replaces the Range.
+                new_indices[pos] = self._create_inlined_idx(
+                    call_node, formal_args,
+                    local_indices[local_idx_posn],
+                    local_decln_start, actual_start)
+            # Each Range corresponds to one dimension of the formal argument.
+            local_idx_posn += 1
+        return new_indices
+
+    def _replace_formal_struc_arg(self, actual_arg, ref, call_node,
+                                  formal_args):
+        '''
+        Called by _replace_formal_arg() whenever a formal or actual argument
+        involves an array or structure access that can't be handled with a
+        simple substitution, e.g.
+
+        .. code-block:: fortran
+
+            call my_sub(my_struc%grid(:,2,:), 10)
+
+            subroutine my_sub(grid, ngrids)
+              ...
+              do igrid = 1, ngrids
+                do jgrid = ...
+                  do i = 1, 10
+                    do j = 1, 10
+                      grid(igrid, jgrid)%data(i,j) = 0.0
+
+        The assignment in the inlined code should become
+
+        .. code-block:: fortran
+
+            my_struc%grid(igrid,2,jgrid)%data(i,j) = 0.0
+
+        This routine therefore recursively combines any References to formal
+        arguments in the supplied Reference (including any array-index
+        expressions) with the corresponding Reference
+        from the call site to make a new Reference for use in the inlined code.
+
+        :param actual_arg: an actual argument to the routine being inlined.
+        :type actual_arg: :py:class:`psyclone.psyir.nodes.Reference`
+        :param ref: the corresponding reference to a formal argument.
+        :type ref: :py:class:`psyclone.psyir.nodes.Reference`
+        :param call_node: the call site.
+        :type call_node: :py:class:`psyclone.psyir.nodes.Call`
+        :param formal_args: the formal arguments of the called routine.
+        :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
+
+        :returns: the replacement reference.
+        :rtype: :py:class:`psyclone.psyir.nodes.Reference`
+
+        '''
+        # The final stage of this method creates a brand new
+        # [ArrayOf]Structure[s]Reference so we have to collect the indices and
+        # members as we walk down both the actual and local references.
+        local_indices = None
+        members = []
+
+        # Actual arg could be var, var(:)%index, var(i,j)%grid(:) or
+        # var(j)%data(i) etc. Any Ranges must correspond to dimensions of the
+        # formal argument. The validate() method has already ensured that we
+        # do not have any indirect accesses or non-unit strides.
+
+        if isinstance(ref, ArrayMixin):
+            local_indices = [idx.copy() for idx in ref.indices]
+
+        # Since a Range can occur at any level of a Structure access in the
+        # actual argument, we walk down it and check each Member. Any Ranges
+        # are updated according to how that dimension is accessed by the
+        # reference inside the routine.
+        cursor = actual_arg
+        while True:
+            if isinstance(cursor, ArrayMixin):
+                new_indices = self._update_actual_indices(
+                    cursor, ref, call_node, formal_args)
+                members.append((cursor.name, new_indices))
+            else:
+                members.append(cursor.name)
+
+            if not isinstance(cursor, (StructureMember, StructureReference)):
+                break
+            cursor = cursor.member
+
+        if not actual_arg.walk(Range) and local_indices:
+            # There are no Ranges in the actual argument but the local
+            # reference is an array access.
+            # Create updated index expressions for that access.
+            new_indices = []
+            for idx in local_indices:
+                new_indices.append(
+                    self._replace_formal_arg(
+                        idx.copy(), call_node, formal_args))
+            # Replace the last entry in the `members` list with a new array
+            # access.
+            members[-1] = (cursor.name, new_indices)
+
+        # We now walk down the *local* access, skipping its head (as that is
+        # replaced by the actual arg). We don't need to worry about updating
+        # index expressions in the actual argument as they are independent of
+        # any array accesses within a structure passed as a formal argument.
+        cursor = ref
+        while isinstance(cursor, (StructureReference, StructureMember)):
+            cursor = cursor.member
+            if isinstance(cursor, ArrayMixin):
+                new_indices = []
+                for idx in cursor.indices:
+                    # Update each index expression in case it refers to
+                    # formal arguments.
+                    new_indices.append(
+                        self._replace_formal_arg(
+                            idx.copy(), call_node, formal_args))
+                members.append((cursor.name, new_indices))
+            else:
+                members.append(cursor.name)
+
+        # Finally, construct the new Reference using the information we've
+        # collected from both the actual argument and local access.
+        if len(members) > 1:
+            # We have some form of Structure reference.
+            if isinstance(members[0], tuple):
+                # Root of access is an array access.
+                return ArrayOfStructuresReference.create(actual_arg.symbol,
+                                                         members[0][1],
+                                                         members[1:])
+            return StructureReference.create(actual_arg.symbol, members[1:])
+
+        # Just an array reference.
+        return ArrayReference.create(actual_arg.symbol, members[0][1])
+
     @staticmethod
     def _inline_container_symbols(table, routine_table):
         '''
@@ -276,16 +635,23 @@ class InlineTrans(Transformation):
             been updated to refer to a Container at the call site.
 
         '''
-        dummy_args = routine_table.argument_list
+        routine_name = routine_table.node.name
+        formal_args = routine_table.argument_list
 
         for old_sym in routine_table.symbols:
 
-            if old_sym in dummy_args or isinstance(old_sym, ContainerSymbol):
+            if old_sym in formal_args or isinstance(old_sym, ContainerSymbol):
                 # We've dealt with Container symbols in
-                # _inline_container_symbols() and we deal with dummy arguments
+                # _inline_container_symbols() and we deal with formal arguments
                 # in apply().
                 continue
 
+            if old_sym.name == routine_name and isinstance(old_sym,
+                                                           RoutineSymbol):
+                # We don't want or need the symbol representing the routine
+                # that is being inlined.
+                continue
+
             old_name = old_sym.name
             try:
                 table.add(old_sym)
@@ -330,22 +696,25 @@ class InlineTrans(Transformation):
         :param options: a dictionary with options for transformations.
         :type options: Optional[Dict[str, Any]]
 
-        :raises TransformationError: if the supplied node is not a Call.
+        :raises TransformationError: if the supplied node is not a Call or is \
+            an IntrinsicCall.
         :raises TransformationError: if the routine has a return value.
         :raises TransformationError: if the routine body contains a Return \
             that is not the first or last statement.
         :raises TransformationError: if the routine body contains a CodeBlock.
         :raises TransformationError: if the called routine has a named \
             argument.
+        :raises TransformationError: if any of the variables declared within \
+            the called routine are of UnknownType.
         :raises TransformationError: if a symbol of a given name is imported \
             from different containers at the call site and within the routine.
         :raises TransformationError: if the routine accesses an un-resolved \
             symbol.
+        :raises TransformationError: if the number of arguments in the call \
+            does not match the number of formal arguments of the routine.
         :raises TransformationError: if a symbol declared in the parent \
             container is accessed in the target routine.
-        :raises TransformationError: if any of the actual arguments represent \
-            an array subsection.
-        :raises TransformationError: if the shape of an array dummy argument \
+        :raises TransformationError: if the shape of an array formal argument \
             does not match that of the corresponding actual argument.
 
         '''
@@ -357,6 +726,9 @@ class InlineTrans(Transformation):
                 f"The target of the InlineTrans transformation "
                 f"should be a Call but found '{type(node).__name__}'.")
 
+        if isinstance(node, IntrinsicCall):
+            raise TransformationError(
+                f"Cannot inline an IntrinsicCall ('{node.routine.name}')")
         name = node.routine.name
 
         # Check that we can find the source of the routine being inlined.
@@ -389,10 +761,20 @@ class InlineTrans(Transformation):
                     f"Routine '{routine.name}' cannot be inlined because it "
                     f"has a named argument '{arg}' (TODO #924).")
 
-        # Check for symbol-naming clashes that we can't handle.
         table = node.scope.symbol_table
         routine_table = routine.symbol_table
 
+        # Check that there are no static variables in the routine (because we
+        # don't know whether the routine is called from other places).
+        # TODO #2008 - at the moment we only check for symbols of UnknownType
+        # which is safe but possibly overkill.
+        for sym in routine_table.local_datasymbols:
+            if isinstance(sym.datatype, UnknownType):
+                raise TransformationError(
+                    f"Routine '{routine.name}' cannot be inlined because it "
+                    f"contains a Symbol '{sym.name}' which is of unknown type:"
+                    f" '{sym.datatype.declaration}')")
+
         # We can't handle a clash between (apparently) different symbols that
         # share a name but are imported from different containers.
         callsite_imports = table.imported_symbols
@@ -413,62 +795,149 @@ class InlineTrans(Transformation):
 
         # Check for unresolved symbols or for any accessed from the Container
         # containing the target routine.
-        refs = routine.walk(Reference)
-        for ref in refs:
-            if ref.symbol.name not in routine_table:
-                sym = routine_table.lookup(ref.symbol.name)
-                if sym.is_unresolved:
+        # TODO #1792 - kind parameters will not be found by simply doing
+        # `walk(Reference)`. Although SymbolTable has the
+        # `precision_datasymbols` property, this only returns those Symbols
+        # that are used to define the precision of other Symbols in the same
+        # table. If a precision symbol is only used within Statements then we
+        # don't currently capture the fact that it is a precision symbol.
+        ref_or_lits = routine.walk((Reference, Literal))
+        # Check for symbols in any constant-value expressions
+        # (Fortran parameters) or array dimensions.
+        for sym in routine_table.local_datasymbols:
+            if sym.is_constant:
+                ref_or_lits.extend(
+                    sym.constant_value.walk((Reference, Literal)))
+            if isinstance(sym.datatype, ArrayType):
+                for dim in sym.shape:
+                    ref_or_lits.extend(dim.lower.walk(Reference, Literal))
+                    ref_or_lits.extend(dim.upper.walk(Reference, Literal))
+        # Keep a reference to each Symbol that we check so that we can avoid
+        # repeatedly checking the same Symbol.
+        _symbol_cache = set()
+        for lnode in ref_or_lits:
+            if isinstance(lnode, Literal):
+                if not isinstance(lnode.datatype.precision, DataSymbol):
+                    continue
+                sym = lnode.datatype.precision
+            else:
+                sym = lnode.symbol
+            # If we've already seen this Symbol then we can skip it.
+            if sym in _symbol_cache:
+                continue
+            _symbol_cache.add(sym)
+            # We haven't seen this Symbol before.
+            if sym.is_unresolved:
+                try:
+                    routine_table.resolve_imports(symbol_target=sym)
+                except KeyError:
+                    # The symbol is not (directly) imported into the symbol
+                    # table local to the routine.
+                    # pylint: disable=raise-missing-from
                     raise TransformationError(
-                        f"Routine '{routine.name}' cannot be inlined because "
-                        f"it accesses an un-resolved variable "
-                        f"'{ref.symbol.name}'.")
-                if not sym.is_import:
+                        f"Routine '{routine.name}' cannot be inlined "
+                        f"because it accesses variable '{sym.name}' and this "
+                        f"cannot be found in any of the containers directly "
+                        f"imported into its symbol table.")
+            else:
+                if sym.name not in routine_table:
                     raise TransformationError(
                         f"Routine '{routine.name}' cannot be inlined because "
-                        f"it accesses variable '{ref.symbol.name}' from its "
+                        f"it accesses variable '{sym.name}' from its "
                         f"parent container.")
 
-        # Check that the shape of any dummy array arguments are the same as
+        # Check that the shapes of any formal array arguments are the same as
         # those at the call site.
         visitor = FortranWriter()
-        for dummy_arg, actual_arg in zip(routine_table.argument_list,
-                                         node.children):
-            # TODO #1799 this really needs the `datatype` method that is
-            # planned for all reference nodes.
-            if isinstance(actual_arg, ArrayReference):
-                rank = 0
-                for idx, expr in enumerate(actual_arg.indices):
-                    if isinstance(expr, Range):
-                        rank += 1
-                        # TODO #924 add a method to Range that returns the
-                        # PSyIR expression for the number of elements it
-                        # contains. This can then be compared with the shape of
-                        # the dummy argument.
-                        if not actual_arg.is_full_range(idx):
-                            # It's OK to use the loop variable in the lambda
-                            # definition because if we get to this point then
-                            # we're going to quit the loop.
-                            # pylint: disable=cell-var-from-loop
-                            raise TransformationError(LazyString(
-                                lambda: f"Cannot inline routine "
-                                f"'{routine.name}' because argument "
-                                f"'{visitor(actual_arg)}' is "
-                                f"an array subsection (TODO #924)."))
-                if isinstance(dummy_arg.datatype, ScalarType):
-                    dummy_rank = 0
-                else:
-                    dummy_rank = len(dummy_arg.datatype.shape)
-                if rank != dummy_rank:
-                    # It's OK to use the loop variable in the lambda definition
-                    # because if we get to this point then we're going to quit
-                    # the loop.
-                    # pylint: disable=cell-var-from-loop
-                    raise TransformationError(LazyString(
+
+        if len(routine_table.argument_list) != len(node.children):
+            raise TransformationError(LazyString(
+                lambda: f"Cannot inline '{visitor(node).strip()}' because"
+                f" the number of arguments supplied to the call "
+                f"({len(node.children)}) does not match the number of "
+                f"arguments the routine is declared to have "
+                f"({len(routine_table.argument_list)})."))
+
+        for formal_arg, actual_arg in zip(routine_table.argument_list,
+                                          node.children):
+            # If the formal argument is an array with non-default bounds then
+            # we also need to know the bounds of that array at the call site.
+            # For this reason, we cannot support formal arguments of
+            # UnknownType because they might be arrays with non-default bounds.
+            if isinstance(formal_arg.datatype, UnknownType):
+                raise TransformationError(
+                    f"Routine '{routine.name}' cannot be inlined because "
+                    f"formal argument '{formal_arg.name}' is of UnknownType")
+
+            if not isinstance(formal_arg.datatype, ArrayType):
+                # Formal argument is not an array so we don't need to do any
+                # further checks.
+                continue
+
+            if not isinstance(actual_arg, (Reference, Literal)):
+                # TODO #1799 this really needs the `datatype` method to be
+                # extended to support all nodes. For now we have to abort
+                # if we encounter an argument that is not a scalar (according
+                # to the corresponding formal argument) but is not a
+                # Reference or a Literal as we don't know whether the result
+                # of any general expression is or is not an array.
+                # pylint: disable=cell-var-from-loop
+                raise TransformationError(LazyString(
+                    lambda: f"The call '{visitor(node)}' cannot be inlined "
+                    f"because actual argument '{visitor(actual_arg)}' "
+                    f"corresponds to a formal argument with array type but is "
+                    f"not a Reference or a Literal."))
+
+            # We have an array argument. We are only able to check that the
+            # argument is not re-shaped in the called routine if we have full
+            # type information on the actual argument.
+            # TODO #924. It would be useful if the `datatype` property was
+            # a method that took an optional 'resolve' argument to indicate
+            # that it should attempt to resolve any DeferredTypes.
+            if isinstance(actual_arg.datatype, (DeferredType, UnknownType)):
+                raise TransformationError(
+                    f"Routine '{routine.name}' cannot be inlined because "
+                    f"the type of the actual argument "
+                    f"'{actual_arg.symbol.name}' corresponding to an array"
+                    f" formal argument ('{formal_arg.name}') is unknown.")
+
+            formal_rank = 0
+            actual_rank = 0
+            if isinstance(formal_arg.datatype, ArrayType):
+                formal_rank = len(formal_arg.datatype.shape)
+            if isinstance(actual_arg.datatype, ArrayType):
+                actual_rank = len(actual_arg.datatype.shape)
+            if formal_rank != actual_rank:
+                # It's OK to use the loop variable in the lambda definition
+                # because if we get to this point then we're going to quit
+                # the loop.
+                # pylint: disable=cell-var-from-loop
+                raise TransformationError(LazyString(
                         lambda: f"Cannot inline routine '{routine.name}' "
                         f"because it reshapes an argument: actual argument "
-                        f"'{visitor(actual_arg)}' has rank {rank} but the "
-                        f"corresponding dummy argument, '{dummy_arg.name}', "
-                        f"has rank {len(dummy_arg.datatype.shape)}"))
+                        f"'{visitor(actual_arg)}' has rank {actual_rank} but "
+                        f"the corresponding formal argument, "
+                        f"'{formal_arg.name}', has rank {formal_rank}"))
+            if actual_rank:
+                ranges = actual_arg.walk(Range)
+                for rge in ranges:
+                    ancestor_ref = rge.ancestor(Reference)
+                    if ancestor_ref is not actual_arg:
+                        # Have a range in an indirect access.
+                        # pylint: disable=cell-var-from-loop
+                        raise TransformationError(LazyString(
+                            lambda: f"Cannot inline routine '{routine.name}' "
+                            f"because argument '{visitor(actual_arg)}' has an "
+                            f"array range in an indirect access (TODO #924)."))
+                    if rge.step != _ONE:
+                        # TODO #1646. We could resolve this problem by making
+                        # a new array and copying the necessary values into it.
+                        # pylint: disable=cell-var-from-loop
+                        raise TransformationError(LazyString(
+                            lambda: f"Cannot inline routine '{routine.name}' "
+                            f"because one of its arguments is an array slice "
+                            f"with a non-unit stride: '{visitor(actual_arg)}' "
+                            f"(TODO #1646)"))
 
     @staticmethod
     def _find_routine(call_node):


Test patch:
diff --git a/src/psyclone/tests/psyir/nodes/array_mixin_test.py b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
index c68d3438c..6e65a4255 100644
--- a/src/psyclone/tests/psyir/nodes/array_mixin_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_mixin_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2022, Science and Technology Facilities Council.
+# Copyright (c) 2022-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -32,41 +32,47 @@
 # POSSIBILITY OF SUCH DAMAGE.
 # -----------------------------------------------------------------------------
 # Author S. Siso, STFC Daresbury Lab
-# Modified: R. W. Ford, STFC Daresbury Lab
+# Modified: R. W. Ford and A. R. Porter, STFC Daresbury Lab
 # -----------------------------------------------------------------------------
 
 ''' Performs py.test tests of the ArrayMixin PSyIR nodes trait. '''
 
 import pytest
 from psyclone.errors import InternalError
-from psyclone.psyir.nodes import ArrayReference, ArrayOfStructuresReference, \
-    BinaryOperation, Range, Literal, Routine, Assignment
-from psyclone.psyir.symbols import DataSymbol, DeferredType, ArrayType, \
-    INTEGER_TYPE
+from psyclone.psyir.nodes import (
+    ArrayOfStructuresReference, ArrayReference, BinaryOperation, Range,
+    Literal, Routine, StructureReference, Assignment, Reference)
+from psyclone.psyir.symbols import (
+    ArrayType, DataSymbol, DataTypeSymbol, DeferredType, INTEGER_TYPE,
+    REAL_TYPE, StructureType, Symbol)
+
+
+_ONE = Literal("1", INTEGER_TYPE)
+_TWO = Literal("2", INTEGER_TYPE)
 
 
 def test_get_outer_range_index():
     '''Check that the get_outer_range_index method returns the outermost index
-    of the children list that is a range. Use ArrayReference and
-    ArrayOfStructuresReference as concrete implementations of ArrayMixins.
+    of the children list that is a range.
+
     '''
     symbol = DataSymbol("my_symbol", ArrayType(INTEGER_TYPE, [10, 10, 10]))
     array = ArrayReference.create(symbol, [Range(), Range(), Range()])
     assert array.get_outer_range_index() == 2
 
     symbol = DataSymbol("my_symbol", DeferredType())
-    aos = ArrayOfStructuresReference.create(
-        symbol, [Range(), Range(), Range()], ["nx"])
-    assert aos.get_outer_range_index() == 3  # +1 for the member child
+    aos = ArrayReference.create(
+        symbol, [_ONE.copy(), Range(), Range(), Range()])
+    assert aos.get_outer_range_index() == 3  # +1 for the Literal child
 
 
 def test_get_outer_range_index_error():
     '''Check that the get_outer_range_index method raises an IndexError if
-    no range exist as child of the given array. Use ArrayReference as concrete
-    implementation of ArrayMixin.
+    no range exist as child of the given array.
+
     '''
     symbol = DataSymbol("my_symbol", ArrayType(INTEGER_TYPE, [10]))
-    array = ArrayReference.create(symbol, [Literal("2", INTEGER_TYPE)])
+    array = ArrayReference.create(symbol, [_TWO.copy()])
     with pytest.raises(IndexError):
         _ = array.get_outer_range_index()
 
@@ -124,6 +130,128 @@ def test_is_upper_lower_bound(fortran_reader):
     assert not array_ref.is_upper_bound(0)
 
 
+def test_get_lbound_expression():
+    '''
+    Tests for the get_lbound_expression method on an ArrayReference defined
+    by an ArrayType.
+
+    '''
+    # Symbol is of ArrayType.
+    lbound = DataSymbol("jmin", INTEGER_TYPE,
+                        constant_value=Literal("3", INTEGER_TYPE))
+    lbnd_ref = Reference(lbound)
+    symbol = DataSymbol("my_symbol", ArrayType(INTEGER_TYPE,
+                                               [10, (2, 10), (lbnd_ref, 10)]))
+    aref = ArrayReference.create(symbol,
+                                 [_ONE.copy(), _ONE.copy(), _ONE.copy()])
+    assert aref.get_lbound_expression(0) == _ONE
+    assert aref.get_lbound_expression(1) == _TWO
+    lb2 = aref.get_lbound_expression(2)
+    assert isinstance(lb2, Reference)
+    # Returned lower bound should be a *copy* of the original.
+    assert lb2 is not lbnd_ref
+    assert lb2.symbol is lbound
+    with pytest.raises(IndexError):
+        aref.get_lbound_expression(3)
+    # Symbol is of DeferredType so the result should be an instance of the
+    # LBOUND intrinsic.
+    dtsym = DataSymbol("oops", DeferredType())
+    dtref = ArrayReference.create(dtsym,
+                                  [_ONE.copy(), _ONE.copy(), _ONE.copy()])
+    lbnd = dtref.get_lbound_expression(1)
+    assert isinstance(lbnd, BinaryOperation)
+    assert lbnd.operator == BinaryOperation.Operator.LBOUND
+    assert lbnd.children[0].symbol is dtsym
+    assert lbnd.children[1] == Literal("2", INTEGER_TYPE)
+
+
+@pytest.mark.parametrize("extent", [ArrayType.Extent.DEFERRED,
+                                    ArrayType.Extent.ATTRIBUTE])
+def test_get_lbound_expression_unknown_size(extent):
+    '''
+    Test the get_lbound_expression method when we have the definition of the
+    array type but its dimensions are unknown.
+
+    '''
+    symbol = DataSymbol("my_symbol", ArrayType(INTEGER_TYPE,
+                                               [extent, extent]))
+    aref = ArrayReference.create(symbol, [_ONE.copy(), _ONE.copy()])
+    lbnd = aref.get_lbound_expression(1)
+    assert isinstance(lbnd, BinaryOperation)
+    assert lbnd.operator == BinaryOperation.Operator.LBOUND
+    assert lbnd.children[0].symbol is symbol
+
+
+def test_aref_to_aos_lbound_expression():
+    '''
+    Test the get_lbound_expression() method for an ArrayReference to an array
+    of structures.
+
+    '''
+    sgrid_type = StructureType.create(
+        [("id", INTEGER_TYPE, Symbol.Visibility.PUBLIC)])
+    sgrid_type_sym = DataTypeSymbol("subgrid_type", sgrid_type)
+    sym = DataSymbol("subgrids", ArrayType(sgrid_type_sym, [(3, 10)]))
+    one = Literal("1", INTEGER_TYPE)
+    lbound = BinaryOperation.create(BinaryOperation.Operator.LBOUND,
+                                    Reference(sym), one)
+    ubound = BinaryOperation.create(BinaryOperation.Operator.UBOUND,
+                                    Reference(sym), one.copy())
+    array = ArrayReference.create(sym, [Range.create(lbound, ubound)])
+    lbnd = array.get_lbound_expression(0)
+    assert lbnd.value == "3"
+
+
+def test_member_get_lbound_expression(fortran_writer):
+    '''
+    Tests for the get_lbound_expression() method when used with a
+    sub-class of Member.
+
+    '''
+    one = Literal("1", INTEGER_TYPE)
+    two = Literal("2", INTEGER_TYPE)
+    # First, test when we don't have type information.
+    grid_type = DataTypeSymbol("grid_type", DeferredType())
+    sym = DataSymbol("grid_var", grid_type)
+    ref = StructureReference.create(sym, [("data", [one.copy()])])
+    lbnd = ref.member.get_lbound_expression(0)
+    assert isinstance(lbnd, BinaryOperation)
+    out = fortran_writer(lbnd).lower()
+    assert out == "lbound(grid_var%data, 1)"
+    usym = DataSymbol("uvar", DeferredType())
+    ref = ArrayOfStructuresReference.create(
+        usym, [one.copy()],
+        [("map", [one.copy(), two.copy()]),
+         ("data", [one.copy()])])
+    lbnd = ref.member.member.get_lbound_expression(0)
+    assert isinstance(lbnd, BinaryOperation)
+    out = fortran_writer(lbnd).lower()
+    assert out == "lbound(uvar(1)%map(1,2)%data, 1)"
+    # Second, test when we do have type information.
+    a2d = ArrayType(REAL_TYPE, [2, (2, 8)])
+    # Structure that contains "map" which is a 2D array.
+    stypedef = StructureType.create(
+        [("map", a2d, Symbol.Visibility.PUBLIC)])
+    stypedefsym = DataTypeSymbol("map_type", stypedef)
+    # Structure containing a structure of stypedef and an array of such
+    # structures.
+    stypedef2 = StructureType.create(
+        [("grid", stypedef, Symbol.Visibility.PUBLIC),
+         ("subgrids", ArrayType(stypedefsym, [3, (2, 6)]),
+          Symbol.Visibility.PUBLIC)])
+    ssym = DataSymbol("var", stypedef2)
+    sref = StructureReference.create(ssym,
+                                     ["grid",
+                                      ("map", [two.copy(), two.copy()])])
+    assert sref.member.member.get_lbound_expression(0) == one
+    assert sref.member.member.get_lbound_expression(1) == two
+    sref2 = StructureReference.create(
+        ssym, [("subgrids", [two.copy(), two.copy()]),
+               ("map", [two.copy(), two.copy()])])
+    assert sref2.member.get_lbound_expression(1) == two
+    assert sref2.member.member.get_lbound_expression(1) == two
+
+
 def test_get_effective_shape(fortran_reader, fortran_writer):
     '''Tests for the _get_effective_shape() method.'''
     code = (
diff --git a/src/psyclone/tests/psyir/nodes/array_reference_test.py b/src/psyclone/tests/psyir/nodes/array_reference_test.py
index cbc905974..f2a7aae87 100644
--- a/src/psyclone/tests/psyir/nodes/array_reference_test.py
+++ b/src/psyclone/tests/psyir/nodes/array_reference_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2019-2022, Science and Technology Facilities Council.
+# Copyright (c) 2019-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -44,8 +44,9 @@ from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.psyir.nodes.node import colored
 from psyclone.psyir.nodes import Reference, ArrayReference, Assignment, \
     Literal, BinaryOperation, Range, KernelSchedule
-from psyclone.psyir.symbols import DataSymbol, ArrayType, ScalarType, \
-    REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, INTEGER_TYPE
+from psyclone.psyir.symbols import (
+    ArrayType, DataSymbol, DataTypeSymbol, DeferredType, ScalarType,
+    REAL_SINGLE_TYPE, INTEGER_SINGLE_TYPE, REAL_TYPE, INTEGER_TYPE)
 from psyclone.tests.utilities import check_links
 
 
@@ -510,6 +511,12 @@ def test_array_datatype(fortran_writer):
     # The easiest way to check the expression is to convert it to Fortran
     code = fortran_writer(upper)
     assert code == "(4 - 2) / 1 + 1"
+    # Reference to a single element of an array of structures.
+    stype = DataTypeSymbol("grid_type", DeferredType())
+    atype = ArrayType(stype, [10])
+    asym = DataSymbol("aos", atype)
+    aref = ArrayReference.create(asym, [two.copy()])
+    assert aref.datatype is stype
 
 
 def test_array_create_colon(fortran_writer):
diff --git a/src/psyclone/tests/psyir/nodes/member_test.py b/src/psyclone/tests/psyir/nodes/member_test.py
index 4ee044c9c..856b4f801 100644
--- a/src/psyclone/tests/psyir/nodes/member_test.py
+++ b/src/psyclone/tests/psyir/nodes/member_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2020-2022, Science and Technology Facilities Council.
+# Copyright (c) 2020-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,7 +37,6 @@
 
 ''' This module contains pytest tests for the Member class. '''
 
-from __future__ import absolute_import
 import pytest
 from psyclone.psyir import nodes
 
diff --git a/src/psyclone/tests/psyir/symbols/symbol_table_test.py b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
index 66e2b4c6f..a89f4cca1 100644
--- a/src/psyclone/tests/psyir/symbols/symbol_table_test.py
+++ b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
@@ -2097,6 +2097,43 @@ def test_resolve_imports_common_symbol(fortran_reader, tmpdir, monkeypatch,
     assert symtab.lookup("common_import").datatype.intrinsic.name == "INTEGER"
 
 
+def test_resolve_imports_parent_scope(fortran_reader, tmpdir, monkeypatch):
+    '''Test that resolve_imports() works as expected if a Symbol is brought
+    into scope from a parent table (which does not itself contain the Symbol
+    in question).'''
+    # Set up include_path to import the proper modules
+    monkeypatch.setattr(Config.get(), '_include_paths', [str(tmpdir)])
+    filename = os.path.join(str(tmpdir), "a_mod.f90")
+    with open(filename, "w", encoding='UTF-8') as module:
+        module.write('''
+        module a_mod
+            integer :: some_var
+            integer, parameter :: wp = kind(1.0)
+        end module a_mod
+        ''')
+    psyir = fortran_reader.psyir_from_source('''
+        module b_mod
+            use a_mod
+            use other_mod
+        contains
+          subroutine my_sub()
+            real(kind=wp) :: rvar
+            some_var = some_var + 1_wp
+          end subroutine
+        end module b_mod
+        ''')
+    mod = psyir.children[0]
+    subroutine = psyir.walk(Routine)[0]
+    lit = subroutine.walk(Literal)[0]
+    sym = lit.datatype.precision
+    mod.symbol_table.resolve_imports(symbol_target=sym)
+    # A new Symbol with the correct properties should have been added to the
+    # table associated with the Container.
+    new_sym = mod.symbol_table.lookup(sym.name)
+    assert isinstance(new_sym.interface, ImportInterface)
+    assert new_sym.interface.container_symbol.name == "a_mod"
+
+
 def test_scope():
     ''' Test that the scope property returns the SymbolTable associated with
     the node. '''
diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
index c0eac98bd..53976ed31 100644
--- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py
+++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2022, Science and Technology Facilities Council.
+# Copyright (c) 2022-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -37,14 +37,35 @@
 '''This module tests the inlining transformation.
 '''
 
+import os
 import pytest
 
+from psyclone.configuration import Config
 from psyclone.errors import InternalError
-from psyclone.psyir.nodes import Call, Routine
+from psyclone.psyir.nodes import Call, IntrinsicCall, Reference, Routine
+from psyclone.psyir.symbols import DataSymbol, DeferredType
 from psyclone.psyir.transformations import (InlineTrans,
                                             TransformationError)
 from psyclone.tests.utilities import Compile
 
+MY_TYPE = ("  integer, parameter :: ngrids = 10\n"
+           "  type other_type\n"
+           "    real, dimension(10) :: data\n"
+           "    integer :: nx\n"
+           "  end type other_type\n"
+           "  type my_type\n"
+           "    integer :: idx\n"
+           "    real, dimension(10) :: data\n"
+           "    real, dimension(5,10) :: data2d\n"
+           "    type(other_type) :: local\n"
+           "  end type my_type\n"
+           "  type big_type\n"
+           "    type(my_type) :: region\n"
+           "  end type big_type\n"
+           "  type vbig_type\n"
+           "    type(big_type), dimension(ngrids) :: grids\n"
+           "  end type vbig_type\n")
+
 
 # init
 
@@ -62,9 +83,9 @@ def test_apply_empty_routine(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -87,9 +108,9 @@ def test_apply_single_return(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -113,9 +134,9 @@ def test_apply_return_then_cb(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -140,12 +161,12 @@ def test_apply_array_arg(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  real :: a(10)\n"
-        "  do i=1,10\n"
-        "    a(i) = 1.0\n"
-        "    call sub(a(i))\n"
-        "  end do\n"
+        "    integer :: i\n"
+        "    real :: a(10)\n"
+        "    do i=1,10\n"
+        "      a(i) = 1.0\n"
+        "      call sub(a(i))\n"
+        "    end do\n"
         "  end subroutine run_it\n"
         "  subroutine sub(x)\n"
         "    real, intent(inout) :: x\n"
@@ -164,29 +185,29 @@ def test_apply_array_arg(fortran_reader, fortran_writer, tmpdir):
     assert Compile(tmpdir).string_compiles(output)
 
 
-def test_apply_struct_array_arg(fortran_reader, fortran_writer, tmpdir):
-    '''Check that apply works correctly when the actual argument is an
-    array within a structure.'''
+def test_apply_array_access(fortran_reader, fortran_writer, tmpdir):
+    '''
+    Check that the apply method works correctly when an array is passed
+    into the routine and then indexed within it.
+
+    '''
     code = (
         "module test_mod\n"
-        "  type my_type\n"
-        "    integer :: idx\n"
-        "    real, dimension(10) :: data\n"
-        "  end type my_type\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  real :: a(10)\n"
-        "  type(my_type) :: grid\n"
-        "  grid%data(:) = 1.0\n"
-        "  do i=1,10\n"
-        "    a(i) = 1.0\n"
-        "    call sub(grid%data(i))\n"
-        "  end do\n"
+        "    integer :: i\n"
+        "    real :: a(10)\n"
+        "    do i=1,10\n"
+        "      call sub(a, i)\n"
+        "    end do\n"
         "  end subroutine run_it\n"
-        "  subroutine sub(x)\n"
-        "    real, intent(inout) :: x\n"
-        "    x = 2.0*x\n"
+        "  subroutine sub(x, ivar)\n"
+        "    real, intent(inout), dimension(10) :: x\n"
+        "    integer, intent(in) :: ivar\n"
+        "    integer :: i\n"
+        "    do i = 1, 10\n"
+        "      x(i) = 2.0*ivar\n"
+        "    end do\n"
         "  end subroutine sub\n"
         "end module test_mod\n")
     psyir = fortran_reader.psyir_from_source(code)
@@ -195,79 +216,723 @@ def test_apply_struct_array_arg(fortran_reader, fortran_writer, tmpdir):
     inline_trans.apply(routine)
     output = fortran_writer(psyir)
     assert ("    do i = 1, 10, 1\n"
-            "      a(i) = 1.0\n"
-            "      grid%data(i) = 2.0 * grid%data(i)\n"
+            "      do i_1 = 1, 10, 1\n"
+            "        a(i_1) = 2.0 * i\n"
+            "      enddo\n" in output)
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_gocean_kern(fortran_reader, fortran_writer, monkeypatch):
+    '''
+    Test the apply method with a typical GOcean kernel.
+
+    TODO #924 - currently this xfails because we don't resolve the type of
+    the actual argument.
+
+    '''
+    code = (
+        "module psy_single_invoke_test\n"
+        "  use field_mod, only: r2d_field\n"
+        "  use kind_params_mod\n"
+        "  implicit none\n"
+        "  contains\n"
+        "  subroutine invoke_0_compute_cu(cu_fld, pf, u_fld)\n"
+        "    type(r2d_field), intent(inout) :: cu_fld, pf, u_fld\n"
+        "    integer j, i\n"
+        "    do j = cu_fld%internal%ystart, cu_fld%internal%ystop, 1\n"
+        "      do i = cu_fld%internal%xstart, cu_fld%internal%xstop, 1\n"
+        "        call compute_cu_code(i, j, cu_fld%data, pf%data, "
+        "u_fld%data)\n"
+        "      end do\n"
+        "    end do\n"
+        "  end subroutine invoke_0_compute_cu\n"
+        "  subroutine compute_cu_code(i, j, cu, p, u)\n"
+        "    implicit none\n"
+        "    integer,  intent(in) :: i, j\n"
+        "    real(go_wp), intent(out), dimension(:,:) :: cu\n"
+        "    real(go_wp), intent(in),  dimension(:,:) :: p, u\n"
+        "    cu(i,j) = 0.5d0*(p(i,j)+p(i-1,j))*u(i,j)\n"
+        "  end subroutine compute_cu_code\n"
+        "end module psy_single_invoke_test\n"
+    )
+    # Set up include_path to import the proper module
+    src_dir = os.path.join(
+        os.path.dirname(os.path.abspath(__file__)),
+        "../../../external/dl_esm_inf/finite_difference/src")
+    monkeypatch.setattr(Config.get(), '_include_paths', [str(src_dir)])
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.apply(psyir.walk(Call)[0])
+    if ("actual argument 'cu_fld' corresponding to an array formal "
+            "argument ('cu') is unknown" in str(err.value)):
+        pytest.xfail(
+            "TODO #924 - extend validation to attempt to resolve type of "
+            "actual argument.")
+    output = fortran_writer(psyir)
+    assert ("    do j = cu_fld%internal%ystart, cu_fld%internal%ystop, 1\n"
+            "      do i = cu_fld%internal%xstart, cu_fld%internal%xstop, 1\n"
+            "        cu_fld%data(i,j) = 0.5d0 * (pf%data(i,j) + "
+            "pf%data(i - 1,j)) * u_fld%data(i,j)\n"
+            "      enddo\n"
+            "    enddo\n" in output)
+
+
+def test_apply_struct_arg(fortran_reader, fortran_writer, tmpdir):
+    '''
+    Check that the apply() method works correctly when the routine argument
+    is a StructureReference containing an ArrayMember which is accessed inside
+    the routine.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    type(my_type) :: var\n"
+        f"    type(my_type) :: var_list(10)\n"
+        f"    type(big_type) :: var2(5)\n"
+        f"    do i=1,5\n"
+        f"      call sub(var, i)\n"
+        f"      call sub(var_list(i), i)\n"
+        f"      call sub(var2(i)%region, i)\n"
+        f"      call sub2(var2)\n"
+        f"    end do\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(x, ivar)\n"
+        f"    type(my_type), intent(inout) :: x\n"
+        f"    integer, intent(in) :: ivar\n"
+        f"    integer :: i\n"
+        f"    do i = 1, 10\n"
+        f"      x%data(i) = 2.0*ivar\n"
+        f"    end do\n"
+        f"    x%data(:) = -1.0\n"
+        f"    x%data = -5.0\n"
+        f"    x%data(1:2) = 0.0\n"
+        f"  end subroutine sub\n"
+        f"  subroutine sub2(x)\n"
+        f"    type(big_type), dimension(:), intent(inout) :: x\n"
+        f"    x(:)%region%local%nx = 0\n"
+        f"  end subroutine sub2\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        inline_trans.apply(routine)
+
+    output = fortran_writer(psyir)
+    assert ("    do i = 1, 5, 1\n"
+            "      do i_3 = 1, 10, 1\n"
+            "        var%data(i_3) = 2.0 * i\n"
+            "      enddo\n"
+            "      var%data(:) = -1.0\n"
+            "      var%data = -5.0\n"
+            "      var%data(1:2) = 0.0\n"
+            "      do i_1 = 1, 10, 1\n"
+            "        var_list(i)%data(i_1) = 2.0 * i\n"
+            "      enddo\n"
+            "      var_list(i)%data(:) = -1.0\n"
+            "      var_list(i)%data = -5.0\n"
+            "      var_list(i)%data(1:2) = 0.0\n"
+            "      do i_2 = 1, 10, 1\n"
+            "        var2(i)%region%data(i_2) = 2.0 * i\n"
+            "      enddo\n"
+            "      var2(i)%region%data(:) = -1.0\n"
+            "      var2(i)%region%data = -5.0\n"
+            "      var2(i)%region%data(1:2) = 0.0\n"
+            "      var2(:)%region%local%nx = 0\n"
             "    enddo\n" in output)
     assert Compile(tmpdir).string_compiles(output)
 
 
-def test_apply_ptr_arg(fortran_reader, fortran_writer, tmpdir):
-    '''Check that apply works correctly when the routine has a pointer
-    argument (which is captured as an UnknownFortranType). '''
+def test_apply_unresolved_struct_arg(fortran_reader, fortran_writer):
+    '''
+    Check that we handle acceptable cases of the type of an argument being
+    unresolved but that we reject the case where we can't be sure of
+    the array indexing.
+
+    '''
     code = (
         "module test_mod\n"
         "contains\n"
-        "subroutine main\n"
-        "  real, target :: var = 0.0\n"
-        "  real, pointer :: ptr => null()\n"
-        "  ptr => var\n"
-        "  call sub(ptr)\n"
-        "end subroutine main\n"
-        "subroutine sub(x)\n"
-        "  real, pointer, intent(inout) :: x\n"
-        "  x = x + 1.0\n"
-        "end subroutine sub\n"
-        "end module test_mod\n"
-    )
+        "  subroutine run_it()\n"
+        "    use some_mod, only: mystery_type, mystery\n"
+        "    integer :: i\n"
+        "    type(mystery_type) :: var3, varr(5)\n"
+        # Unknown structure type but array dims are known.
+        "    call sub3(varr)\n"
+        # Unknown actual argument corresponding to a formal array argument
+        # so we can't be sure that it isn't being reshaped.
+        "    call sub3(mystery)\n"
+        # Unknown actual argument corresponding to a formal scalar argument
+        # so lack of type information isn't a problem.
+        "    call sub3a(mystery)\n"
+        # Formal arg specifies array bounds and we don't have them for
+        # the actual argument.
+        "    call sub4(mystery)\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub3(x)\n"
+        "    use some_mod, only: mystery_type\n"
+        "    type(mystery_type), dimension(:), intent(inout) :: x\n"
+        "    x(:)%region%local%nx = 0\n"
+        "  end subroutine sub3\n"
+        "  subroutine sub3a(x)\n"
+        "    use some_mod, only: mystery_type\n"
+        "    type(mystery_type) :: x\n"
+        "    x%flag = 1\n"
+        "  end subroutine sub3a\n"
+        "  subroutine sub4(x)\n"
+        "    use some_mod, only: mystery_type\n"
+        "    type(mystery_type), dimension(3:5), intent(inout) :: x\n"
+        "    x(:)%region%local%nx = 0\n"
+        "  end subroutine sub4\n"
+        "end module test_mod\n")
     psyir = fortran_reader.psyir_from_source(code)
-    call = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
-    inline_trans.apply(call)
+    calls = psyir.walk(Call)
+    # First one should be fine.
+    inline_trans.apply(calls[0])
+    # Second one should fail.
+    with pytest.raises(TransformationError) as err:
+        inline_trans.apply(calls[1])
+    assert ("Routine 'sub3' cannot be inlined because the type of the actual "
+            "argument 'mystery' corresponding to an array formal argument "
+            "('x') is unknown" in str(err.value))
+    # Third one should be fine because it is a scalar argument.
+    inline_trans.apply(calls[2])
+    # We can't do the fourth one.
+    with pytest.raises(TransformationError) as err:
+        inline_trans.apply(calls[3])
+    assert ("Routine 'sub4' cannot be inlined because the type of the actual "
+            "argument 'mystery' corresponding to an array formal argument "
+            "('x') is unknown." in str(err.value))
+    output = fortran_writer(psyir)
+    assert ("    varr(:)%region%local%nx = 0\n"
+            "    call sub3(mystery)\n"
+            "    mystery%flag = 1\n"
+            "    call sub4(mystery)\n" in output)
+
+
+def test_apply_struct_slice_arg(fortran_reader, fortran_writer, tmpdir):
+    '''
+    Check that the apply() method works correctly when there are slices in
+    structure accesses in both the actual and formal arguments.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    type(my_type) :: var_list(10)\n"
+        f"    type(vbig_type), dimension(5) :: cvar\n"
+        f"    call sub(var_list(:)%local%nx, i)\n"
+        f"    call sub2(var_list(:), 1, 1)\n"
+        f"    call sub2(var_list(:), i, i+2)\n"
+        f"    call sub3(cvar)\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(ix, indx)\n"
+        f"    integer, dimension(:) :: ix\n"
+        f"    integer, intent(in) :: indx\n"
+        f"    ix(:) = ix(:) + 1\n"
+        f"  end subroutine sub\n"
+        f"  subroutine sub2(x, start, stop)\n"
+        f"    type(my_type), dimension(:) :: x\n"
+        f"    integer :: start, stop\n"
+        f"    x(:)%data(2) = 0.0\n"
+        f"    x(:)%local%nx = 4\n"
+        f"    x(start:stop+1)%local%nx = -2\n"
+        f"  end subroutine sub2\n"
+        f"  subroutine sub3(y)\n"
+        f"    type(vbig_type), dimension(:) :: y\n"
+        f"    y(2)%grids(2)%region%data(:) = 0.0\n"
+        f"  end subroutine sub3\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        inline_trans.apply(routine)
+    output = fortran_writer(psyir)
+    assert "var_list(:)%local%nx = var_list(:)%local%nx + 1" in output
+    assert "var_list(:)%data(2) = 0.0" in output
+    assert "var_list(:)%local%nx = 4" in output
+    assert "var_list(:1 + 1)%local%nx = -2" in output
+    assert "cvar(2)%grids(2)%region%data(:) = 0.0" in output
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_struct_local_limits_caller(fortran_reader, fortran_writer,
+                                          tmpdir):
+    '''
+    Test the apply() method when there are array bounds specified in the
+    caller.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    type(my_type) :: var_list(10)\n"
+        f"    call sub2(var_list(3:7), 5, 6)\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub2(x, start, stop)\n"
+        f"    type(my_type), dimension(:) :: x\n"
+        f"    integer :: start, stop\n"
+        f"    x(:)%data(2) = 1.0\n"
+        f"    x(:)%local%nx = 3\n"
+        f"    x(start:stop+1)%local%nx = -2\n"
+        f"  end subroutine sub2\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        inline_trans.apply(routine)
     output = fortran_writer(psyir)
-    assert ("    ptr => var\n"
-            "    ptr = ptr + 1.0\n\n"
-            "  end subroutine main\n" in output)
+    assert "var_list(3:7)%data(2) = 1.0" in output
+    assert "var_list(3:7)%local%nx = 3" in output
+    # Element 1 in routine corresponds to element 3 in caller
+    assert "var_list(5 - 1 + 3:6 + 1 - 1 + 3)%local%nx = -2" in output
     assert Compile(tmpdir).string_compiles(output)
 
 
-def test_apply_saved_var(fortran_reader, fortran_writer, tmpdir):
+def test_apply_struct_local_limits_caller_decln(fortran_reader, fortran_writer,
+                                                tmpdir):
     '''
-    Test that a subroutine with a 'save'd variable is inlined correctly.
+    Test the apply() method when there are non-default array bounds specified
+    in the declaration at the call site.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    type(my_type), dimension(2:9) :: varat2\n"
+        f"    real, dimension(4:8) :: varat3\n"
+        f"    call sub2(varat2(:), 5, 6)\n"
+        f"    call sub2(varat2(3:8), 5, 6)\n"
+        f"    call sub3(varat3(5:6))\n"
+        f"    call sub3(varat3)\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub2(x, start, stop)\n"
+        f"    type(my_type), dimension(:) :: x\n"
+        f"    integer :: start, stop\n"
+        f"    x(:)%data(2) = 1.0\n"
+        f"    x(:)%local%nx = 3\n"
+        f"    x(start:stop+1)%local%nx = -2\n"
+        f"  end subroutine sub2\n"
+        f"  subroutine sub3(x)\n"
+        f"    real, dimension(:) :: x\n"
+        f"    x(1:2) = 4.0\n"
+        f"  end subroutine sub3\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        inline_trans.apply(routine)
+    output = fortran_writer(psyir)
+    # Actual declared range is non-default.
+    assert "varat2(:)%data(2) = 1.0\n" in output
+    assert "varat2(:)%local%nx = 3\n" in output
+    # A local access of '1' corresponds to the start of the array which is
+    # index '2' at the call site.
+    assert "varat2(5 - 1 + 2:6 + 1 - 1 + 2)%local%nx = -2\n" in output
+    # Actual arg. has non-default range - index 1 in the routine corresponds
+    # to index 3 at the call site.
+    assert "varat2(3:8)%data(2) = 1.0\n" in output
+    assert "varat2(3:8)%local%nx = 3\n" in output
+    assert "varat2(5 - 1 + 3:6 + 1 - 1 + 3)%local%nx = -2" in output
+    assert "varat3(1 - 1 + 5:2 - 1 + 5) = 4.0\n" in output
+    assert "varat3(1 - 1 + 4:2 - 1 + 4) = 4.0\n" in output
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_struct_local_limits_routine(fortran_reader, fortran_writer,
+                                           tmpdir):
+    '''
+    Test the apply() method when there are non-default array bounds specified
+    in the declaration within the called routine.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    type(my_type) :: var_list(10)\n"
+        f"    type(my_type), dimension(2:8) :: varat2\n"
+        f"    call sub3(var_list(:), 5, 6)\n"
+        f"    call sub3(varat2(:), 5, 6)\n"
+        f"    call sub3(varat2(3:7), 4, 5)\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub3(y, start, stop)\n"
+        f"    type(my_type), dimension(4:6) :: y\n"
+        f"    integer :: start, stop\n"
+        f"    y(:)%data(2) = 2.0\n"
+        f"    y(4:5)%local%nx = 4\n"
+        f"    y(start:stop+1)%local%nx = -3\n"
+        f"  end subroutine sub3\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        inline_trans.apply(routine)
+    output = fortran_writer(psyir)
+    # Access within routine is to full range but formal arg. is declared with
+    # explicit bounds so these have to be taken into account.
+    assert "var_list(4 - 4 + 1:6 - 4 + 1)%data(2) = 2.0" in output
+    assert "var_list(4 - 4 + 1:5 - 4 + 1)%local%nx = 4" in output
+    # Element 4 in routine corresponds to element 1 in caller
+    assert "var_list(5 - 4 + 1:6 + 1 - 4 + 1)%local%nx = -3" in output
+    # Custom limits  in declarations for both formal and actual.
+    # Element 4 in routine corresponds to element 2 in caller.
+    assert "varat2(4 - 4 + 2:6 - 4 + 2)%data(2) = 2.0\n" in output
+    assert "varat2(4 - 4 + 2:5 - 4 + 2)%local%nx = 4\n" in output
+    # A local access of '1' corresponds to the start of the array which is
+    # index '2' at the call site.
+    assert "varat2(5 - 4 + 2:6 + 1 - 4 + 2)%local%nx = -3\n" in output
+    # Actual arg. has non-default range in slice. Therefore index 3 at the
+    # call site becomes index 4 in the routine.
+    assert "varat2(4 - 4 + 3:6 - 4 + 3)%data(2) = 2.0\n" in output
+    assert "varat2(4 - 4 + 3:5 - 4 + 3)%local%nx = 4\n" in output
+    assert "varat2(4 - 4 + 3:5 + 1 - 4 + 3)%local%nx = -3" in output
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_allocatable_array_arg(fortran_reader, fortran_writer, tmpdir):
+    '''
+    Check that apply() works correctly when a formal argument is given the
+    ALLOCATABLE attribute (meaning that the bounds of the formal argument
+    are those of the actual argument).
+
     '''
     code = (
         "module test_mod\n"
+        "  type my_type\n"
+        # TODO #2053 - if the 'data' attribute is correctly given the
+        # 'allocatable' attribute then the whole type ends up as an
+        # UnknownFortranType. For now we therefore omit the 'allocatable'
+        # attribute. This means that the Fortran is not strictly correct
+        # and we can't compile the code.
+        # "    real, allocatable, dimension(:,:) :: data\n"
+        "    real, dimension(:,:) :: data\n"
+        "  end type my_type\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  real :: a(10)\n"
-        "  do i=1,10\n"
-        "    a(i) = 1.0\n"
-        "    call sub(a(i))\n"
-        "  end do\n"
+        "    type(my_type) :: grid\n"
+        "    integer :: jim1, jjp1, jim2, jjp2\n"
+        "    real, allocatable, dimension(:,:) :: avar\n"
+        "    allocate(grid%data(2:6,-1:8))\n"
+        # TODO #1858 - ideally 'grid%data' would work below (instead of
+        # 'grid%data(:,:)') but Reference2ArrayRangeTrans doesn't yet work for
+        # members of structures.
+        "    call sub1(grid%data(:,:), jim1, jjp1)\n"
+        "    call sub1(grid%data(2:6,-1:8), jim2, jjp2)\n"
         "  end subroutine run_it\n"
-        "  subroutine sub(x)\n"
-        "    real, intent(inout) :: x\n"
-        "    real, save :: state = 0.0\n"
-        "    state = state + x\n"
-        "    x = 2.0*x + state\n"
-        "  end subroutine sub\n"
+        "  subroutine sub1(x, ji, jj)\n"
+        "    integer, intent(in) :: ji, jj\n"
+        "    real, dimension(:,:), allocatable :: x\n"
+        "    x(2,-1) = 0.0\n"
+        "    x(ji+2,jj+1) = -1.0\n"
+        "  end subroutine sub1\n"
+        "end module test_mod\n"
+        )
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for routine in psyir.walk(Call):
+        if not isinstance(routine, IntrinsicCall):
+            inline_trans.apply(routine)
+    output = fortran_writer(psyir)
+    # Array index expressions should not be shifted when inlined as the
+    # array bounds are the same.
+    assert "grid%data(2,-1) = 0.0\n" in output
+    assert "grid%data(jim1 + 2,jjp1 + 1) = -1.0\n" in output
+    assert "grid%data(jim2 + 2,jjp2 + 1) = -1.0\n" in output
+    # TODO #2053 - we can't compile this code because the *input* isn't
+    # valid Fortran (see earlier).
+    # assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_array_slice_arg(fortran_reader, fortran_writer, tmpdir):
+    '''
+    Check that the apply() method works correctly when an array slice is
+    passed to a routine and then accessed within it.
+
+    '''
+    code = (
+        "module test_mod\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    integer :: i\n"
+        "    real :: a(5,10,10), b(10,10)\n"
+        "    do i=1,10\n"
+        "      call sub1(a(1,:,i))\n"
+        "    end do\n"
+        "    call sub1a(a(1,1,:))\n"
+        "    call sub2(a(:,1,:))\n"
+        "    call sub2(b)\n"
+        "    call sub2a(b(:,1:5))\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub1(x)\n"
+        "    real, intent(inout), dimension(10) :: x\n"
+        "    integer :: i\n"
+        "    do i = 1, 10\n"
+        "      x(i) = 2.0*i\n"
+        "    end do\n"
+        "  end subroutine sub1\n"
+        "  subroutine sub1a(x)\n"
+        "    real, intent(inout), dimension(10) :: x\n"
+        "    x(1:10) = 3.0 * x(1:10)\n"
+        "  end subroutine sub1a\n"
+        "  subroutine sub2(x)\n"
+        "    real, intent(inout), dimension(5,10) :: x\n"
+        "    integer :: i\n"
+        "    x = 2.0 * x\n"
+        "  end subroutine sub2\n"
+        "  subroutine sub2a(x)\n"
+        "    real, intent(inout), dimension(10,5) :: x\n"
+        "    integer :: i\n"
+        "    do i=1, 10\n"
+        "      x(i,:) = 2.0 * x(i,:)\n"
+        "    end do\n"
+        "  end subroutine sub2a\n"
         "end module test_mod\n")
     psyir = fortran_reader.psyir_from_source(code)
-    routine = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
-    inline_trans.apply(routine)
+    for call in psyir.walk(Call):
+        inline_trans.apply(call)
+    output = fortran_writer(psyir)
+    assert ("    do i = 1, 10, 1\n"
+            "      do i_4 = 1, 10, 1\n"
+            "        a(1,i_4,i) = 2.0 * i_4\n"
+            "      enddo\n"
+            "    enddo\n"
+            "    a(1,1,:) = 3.0 * a(1,1,:)\n"
+            "    a(:,1,:) = 2.0 * a(:,1,:)\n"
+            "    b(:,:) = 2.0 * b(:,:)\n"
+            "    do i_3 = 1, 10, 1\n"
+            "      b(i_3,:5) = 2.0 * b(i_3,:5)\n" in output)
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_struct_array_arg(fortran_reader, fortran_writer, tmpdir):
+    '''Check that apply works correctly when the actual argument is an
+    array element within a structure.'''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i, ig\n"
+        f"    real :: a(10)\n"
+        f"    type(my_type) :: grid\n"
+        f"    type(my_type), dimension(5) :: grid_list\n"
+        f"    grid%data(:) = 1.0\n"
+        f"    do i=1,10\n"
+        f"      a(i) = 1.0\n"
+        f"      call sub(grid%data(i))\n"
+        f"    end do\n"
+        f"    do i=1,10\n"
+        f"      ig = min(i, 5)\n"
+        f"      call sub(grid_list(ig)%data(i))\n"
+        f"    end do\n"
+        f"    do i=1,10\n"
+        f"      ig = min(i, 5)\n"
+        f"      call sub(grid_list(ig)%local%data(i))\n"
+        f"    end do\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(x)\n"
+        f"    real, intent(inout) :: x\n"
+        f"    x = 2.0*x\n"
+        f"  end subroutine sub\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    routines = psyir.walk(Call)
+    inline_trans = InlineTrans()
+    inline_trans.apply(routines[0])
+    inline_trans.apply(routines[1])
+    inline_trans.apply(routines[2])
     output = fortran_writer(psyir).lower()
-    assert ("  subroutine run_it()\n"
-            "    integer :: i\n"
-            "    real, dimension(10) :: a\n"
-            "    real, save :: state = 0.0\n" in output)
-    assert ("      a(i) = 1.0\n"
-            "      state = state + a(i)\n"
-            "      a(i) = 2.0 * a(i) + state\n"
+    assert ("    do i = 1, 10, 1\n"
+            "      a(i) = 1.0\n"
+            "      grid%data(i) = 2.0 * grid%data(i)\n"
+            "    enddo\n" in output)
+    assert ("    do i = 1, 10, 1\n"
+            "      ig = min(i, 5)\n"
+            "      grid_list(ig)%data(i) = 2.0 * grid_list(ig)%data(i)\n"
+            "    enddo\n" in output)
+    assert ("    do i = 1, 10, 1\n"
+            "      ig = min(i, 5)\n"
+            "      grid_list(ig)%local%data(i) = 2.0 * "
+            "grid_list(ig)%local%data(i)\n"
+            "    enddo\n" in output)
+    assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_struct_array_slice_arg(fortran_reader, fortran_writer, tmpdir):
+    '''Check that apply works correctly when the actual argument is an
+    array slice within a structure.'''
+    code = (
+        f"module test_mod\n"
+        f"{MY_TYPE}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    real :: a(10)\n"
+        f"    type(my_type) :: grid\n"
+        f"    type(vbig_type) :: micah\n"
+        f"    grid%data(:) = 1.0\n"
+        f"    grid%data2d(:,:) = 1.0\n"
+        f"    do i=1,10\n"
+        f"      a(i) = 1.0\n"
+        f"      call sub(grid%local%data)\n"
+        f"      call sub(micah%grids(3)%region%data(:))\n"
+        f"      call sub(grid%data2d(:,i))\n"
+        f"      call sub(grid%data2d(1:5,i))\n"
+        f"    end do\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(x)\n"
+        f"    real, dimension(:), intent(inout) :: x\n"
+        f"    integer ji\n"
+        f"    do ji = 1, 5\n"
+        f"      x(ji) = 2.0*x(ji)\n"
+        f"    end do\n"
+        f"    x(1:2) = 0.0\n"
+        f"    x(:) = 3.0\n"
+        f"    x = 5.0\n"
+        f"  end subroutine sub\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for call in psyir.walk(Call):
+        inline_trans.apply(call)
+    output = fortran_writer(psyir)
+    assert ("    do i = 1, 10, 1\n"
+            "      a(i) = 1.0\n"
+            "      do ji = 1, 5, 1\n"
+            "        grid%local%data(ji) = 2.0 * grid%local%data(ji)\n"
+            "      enddo\n"
+            "      grid%local%data(1:2) = 0.0\n"
+            "      grid%local%data(:) = 3.0\n"
+            "      grid%local%data = 5.0\n"
+            "      do ji_1 = 1, 5, 1\n"
+            "        micah%grids(3)%region%data(ji_1) = 2.0 * "
+            "micah%grids(3)%region%data(ji_1)\n"
+            "      enddo\n"
+            "      micah%grids(3)%region%data(1:2) = 0.0\n"
+            "      micah%grids(3)%region%data(:) = 3.0\n"
+            "      micah%grids(3)%region%data(:) = 5.0\n"
+            "      do ji_2 = 1, 5, 1\n"
+            "        grid%data2d(ji_2,i) = 2.0 * grid%data2d(ji_2,i)\n"
+            "      enddo\n"
+            "      grid%data2d(1:2,i) = 0.0\n"
+            "      grid%data2d(:,i) = 3.0\n"
+            "      grid%data2d(:,i) = 5.0\n"
+            "      do ji_3 = 1, 5, 1\n"
+            "        grid%data2d(ji_3,i) = 2.0 * grid%data2d(ji_3,i)\n"
+            "      enddo\n"
+            "      grid%data2d(1:2,i) = 0.0\n"
+            "      grid%data2d(1:5,i) = 3.0\n"
+            "      grid%data2d(1:5,i) = 5.0\n"
             "    enddo\n" in output)
     assert Compile(tmpdir).string_compiles(output)
 
 
+@pytest.mark.parametrize("type_decln", [MY_TYPE, "  use some_mod\n"])
+def test_apply_struct_array(fortran_reader, fortran_writer, tmpdir,
+                            type_decln):
+    '''Test that apply works correctly when the formal argument is an
+    array of structures. We test both when the type of the structure is
+    resolved and when it isn't. In the latter case we cannot perform
+    inlining because we don't know the array bounds at the call site.
+
+    '''
+    code = (
+        f"module test_mod\n"
+        f"{type_decln}"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    integer :: i\n"
+        f"    real :: a(10)\n"
+        f"    type(my_type) :: grid\n"
+        f"    type(vbig_type) :: micah\n"
+        f"    call sub(micah%grids(:))\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(x)\n"
+        f"    type(big_type), dimension(2:4) :: x\n"
+        f"    integer ji\n"
+        f"    ji = 2\n"
+        f"    x(:)%region%idx = 3.0\n"
+        f"    x(ji)%region%idx = 2.0\n"
+        f"  end subroutine sub\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    if "use some_mod" in type_decln:
+        with pytest.raises(TransformationError) as err:
+            inline_trans.apply(psyir.walk(Call)[0])
+        assert ("Routine 'sub' cannot be inlined because the type of the "
+                "actual argument 'micah' corresponding to an array formal "
+                "argument ('x') is unknown." in str(err.value))
+    else:
+        inline_trans.apply(psyir.walk(Call)[0])
+        output = fortran_writer(psyir)
+        assert ("    ji = 2\n"
+                "    micah%grids(2 - 2 + 1:4 - 2 + 1)%region%idx = 3.0\n"
+                "    micah%grids(ji - 2 + 1)%region%idx = 2.0\n" in output)
+        assert Compile(tmpdir).string_compiles(output)
+
+
+def test_apply_repeated_module_use(fortran_reader, fortran_writer):
+    '''
+    Check that any module use statements are not duplicated when
+    multiple calls are inlined.
+
+    '''
+    code = (
+        "module test_mod\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    integer :: i\n"
+        "    real :: a(10,10), b(10,10)\n"
+        "    do i = 1, 10\n"
+        "      call sub2(a(:,i))\n"
+        "    end do\n"
+        "    call sub1(b(:,2))\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub1(x)\n"
+        "    use model_mod, only: radius\n"
+        "    real, intent(inout), dimension(10) :: x\n"
+        "    x(:) = radius\n"
+        "  end subroutine sub1\n"
+        "  subroutine sub2(x)\n"
+        "    use model_mod, only: radius\n"
+        "    real, intent(inout), dimension(10) :: x\n"
+        "    x(:) = 4*radius\n"
+        "  end subroutine sub2\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    for call in psyir.walk(Call):
+        inline_trans.apply(call)
+    output = fortran_writer(psyir)
+    if "use model_mod_1" in output:
+        pytest.xfail("TODO #2005 - bug in flattening of nested symbol tables")
+    assert ("  subroutine run_it()\n"
+            "    use model_mod, only : radius\n"
+            "    integer :: i\n" in output)
+    assert ("    do i = 1, 10, 1\n"
+            "      a(:,i) = 4 * radius\n"
+            "    enddo\n"
+            "    b(:,2) = radius\n" in output)
+
+
 def test_apply_name_clash(fortran_reader, fortran_writer, tmpdir):
     ''' Check that apply() correctly handles the case where a symbol
     in the routine to be in-lined clashes with an existing symbol. '''
@@ -307,9 +972,9 @@ def test_apply_imported_symbols(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use some_mod, only: var2\n"
@@ -337,9 +1002,9 @@ def test_apply_last_stmt_is_return(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -366,9 +1031,9 @@ def test_apply_call_args(fortran_reader, fortran_writer):
         " use kinds_mod, only: i_def\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i, 2*i, 5_i_def)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i, 2*i, 5_i_def)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx, incr1, incr2)\n"
         "    integer, intent(inout) :: idx\n"
@@ -396,10 +1061,10 @@ def test_apply_duplicate_imports(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  use kinds_mod, only: i_def\n"
-        "  integer :: i\n"
-        "  i = 10_i_def\n"
-        "  call sub(i)\n"
+        "    use kinds_mod, only: i_def\n"
+        "    integer :: i\n"
+        "    i = 10_i_def\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use kinds_mod, only: i_def\n"
@@ -428,15 +1093,15 @@ def test_apply_wildcard_import(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  use kinds_mod, only: i_def\n"
-        "  integer :: i\n"
-        "  i = 10_i_def\n"
-        "  call sub(i)\n"
+        "    use kinds_mod, only: i_def\n"
+        "    integer :: i\n"
+        "    i = 10_i_def\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use kinds_mod\n"
         "    integer, intent(inout) :: idx\n"
-        "    idx = idx + 5_i_def\n"
+        "    idx = idx + 5\n"
         "  end subroutine sub\n"
         "end module test_mod\n")
     psyir = fortran_reader.psyir_from_source(code)
@@ -458,10 +1123,10 @@ def test_apply_import_union(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  use kinds_mod, only: r_def\n"
-        "  integer :: i\n"
-        "  i = 10.0_r_def\n"
-        "  call sub(i)\n"
+        "    use kinds_mod, only: r_def\n"
+        "    integer :: i\n"
+        "    i = 10.0_r_def\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use kinds_mod, only: i_def\n"
@@ -489,12 +1154,12 @@ def test_apply_callsite_rename(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  use kinds_mod, only: r_def\n"
-        "  integer :: i, a_clash\n"
-        "  a_clash = 2\n"
-        "  i = 10.0_r_def\n"
-        "  call sub(i)\n"
-        "  i = i * a_clash\n"
+        "    use kinds_mod, only: r_def\n"
+        "    integer :: i, a_clash\n"
+        "    a_clash = 2\n"
+        "    i = 10.0_r_def\n"
+        "    call sub(i)\n"
+        "    i = i * a_clash\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use a_mod, only: a_clash\n"
@@ -526,12 +1191,12 @@ def test_apply_callsite_rename_container(fortran_reader, fortran_writer):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  use kinds_mod, only: r_def\n"
-        "  integer :: i, a_mod\n"
-        "  a_mod = 2\n"
-        "  i = 10.0_r_def\n"
-        "  call sub(i)\n"
-        "  i = i * a_mod\n"
+        "    use kinds_mod, only: r_def\n"
+        "    integer :: i, a_mod\n"
+        "    a_mod = 2\n"
+        "    i = 10.0_r_def\n"
+        "    call sub(i)\n"
+        "    i = i * a_mod\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    use a_mod, only: a_clash\n"
@@ -589,8 +1254,8 @@ def test_inline_symbols_check(fortran_reader):
             "refer to that container at the call site." in str(err.value))
 
 
-def test_inline_non_local_import(fortran_reader, fortran_writer):
-    '''Test that we correctly handle the case where the routine to be
+def test_validate_non_local_import(fortran_reader):
+    '''Test that we reject the case where the routine to be
     inlined accesses a symbol from an import in its parent container.'''
     code = (
         "module test_mod\n"
@@ -609,12 +1274,10 @@ def test_inline_non_local_import(fortran_reader, fortran_writer):
     psyir = fortran_reader.psyir_from_source(code)
     call = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
-    inline_trans.apply(call)
-    output = fortran_writer(psyir)
-    assert ("  subroutine run_it()\n"
-            "    integer :: i\n\n"
-            "    i = 10\n"
-            "    i = i + trouble\n" in output)
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("Routine 'sub' cannot be inlined because it accesses variable "
+            "'trouble' from its parent container." in str(err.value))
 
 
 def test_apply_function(fortran_reader, fortran_writer, tmpdir):
@@ -626,8 +1289,8 @@ def test_apply_function(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  real :: a,b\n"
-        "  a = func(b)\n"
+        "    real :: a,b\n"
+        "    a = func(b)\n"
         "  end subroutine run_it\n"
         "  real function func(b)\n"
         "    real :: b\n"
@@ -665,8 +1328,8 @@ def test_apply_function_declare_name(
         f"module test_mod\n"
         f"contains\n"
         f"  subroutine run_it()\n"
-        f"  real :: a,b\n"
-        f"  a = func(b)\n"
+        f"    real :: a,b\n"
+        f"    a = func(b)\n"
         f"  end subroutine run_it\n"
         f"{function_header}"
         f"    real :: b\n"
@@ -699,8 +1362,8 @@ def test_apply_function_expression(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  real :: a,b\n"
-        "  a = (a*func(b)+2.0)/a\n"
+        "    real :: a,b\n"
+        "    a = (a*func(b)+2.0)/a\n"
         "  end subroutine run_it\n"
         "  real function func(b) result(x)\n"
         "    real :: b\n"
@@ -731,9 +1394,9 @@ def test_apply_multi_function(fortran_reader, fortran_writer, tmpdir):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  real :: a,b,c\n"
-        "  a = func(b)\n"
-        "  c = func(a)\n"
+        "    real :: a,b,c\n"
+        "    a = func(b)\n"
+        "    c = func(a)\n"
         "  end subroutine run_it\n"
         "  real function func(b)\n"
         "    real :: b\n"
@@ -774,7 +1437,8 @@ def test_apply_multi_function(fortran_reader, fortran_writer, tmpdir):
 @pytest.mark.parametrize("start, end, indent", [
     ("", "", ""),
     ("module test_mod\ncontains\n", "end module test_mod\n", "  "),
-    ("module test_mod\nuse dummy\ncontains\n", "end module test_mod\n", "  ")])
+    ("module test_mod\nuse formal\ncontains\n", "end module test_mod\n",
+     "  ")])
 def test_apply_raw_subroutine(
         fortran_reader, fortran_writer, tmpdir, start, end, indent):
     '''Test the apply method works correctly when the routine to be
@@ -806,8 +1470,8 @@ def test_apply_raw_subroutine(
         f"{indent}  a = 2.0 * a\n\n"
         f"{indent}end subroutine run_it\n")
     assert expected in output
-    if "use dummy" not in output:
-        # Compilation will not work with "use dummy" as there is no
+    if "use formal" not in output:
+        # Compilation will not work with "use formal" as there is no
         # mod file.
         assert Compile(tmpdir).string_compiles(output)
 
@@ -870,6 +1534,12 @@ def test_validate_node():
         inline_trans.validate(None)
     assert ("The target of the InlineTrans transformation should be "
             "a Call but found 'NoneType'." in str(info.value))
+    call = IntrinsicCall.create(IntrinsicCall.Intrinsic.ALLOCATE,
+                                [Reference(DataSymbol("array",
+                                                      DeferredType()))])
+    with pytest.raises(TransformationError) as info:
+        inline_trans.validate(call)
+    assert "Cannot inline an IntrinsicCall ('ALLOCATE')" in str(info.value)
 
 
 def test_validate_calls_find_routine(fortran_reader):
@@ -883,9 +1553,9 @@ def test_validate_calls_find_routine(fortran_reader):
         "  use some_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "end module test_mod\n")
     psyir = fortran_reader.psyir_from_source(code)
@@ -906,9 +1576,9 @@ def test_validate_return_stmt(fortran_reader):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -933,9 +1603,9 @@ def test_validate_codeblock(fortran_reader):
         "module test_mod\n"
         "contains\n"
         "  subroutine run_it()\n"
-        "  integer :: i\n"
-        "  i = 10\n"
-        "  call sub(i)\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
         "  end subroutine run_it\n"
         "  subroutine sub(idx)\n"
         "    integer :: idx\n"
@@ -951,6 +1621,120 @@ def test_validate_codeblock(fortran_reader):
             "cannot be inlined" in str(err.value))
 
 
+def test_validate_saved_var(fortran_reader):
+    '''
+    Test that validate rejects a subroutine with a 'save'd variable.
+
+    '''
+    code = (
+        "module test_mod\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    real :: a(10)\n"
+        "    call sub(a(1))\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub(x)\n"
+        "    real, intent(inout) :: x\n"
+        "    real, save :: state = 0.0\n"
+        "    state = state + x\n"
+        "    x = 2.0*x + state\n"
+        "  end subroutine sub\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    routine = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(routine)
+    assert ("Routine 'sub' cannot be inlined because it contains a Symbol "
+            "'state' which is of unknown type: 'REAL, SAVE :: state"
+            in str(err.value))
+
+
+@pytest.mark.parametrize("code_body", ["idx = idx + 5_i_def",
+                                       "real, parameter :: pi = 3_wp\n"
+                                       "idx = idx + 1\n"])
+def test_validate_unresolved_precision_sym(fortran_reader, code_body):
+    '''Test that a routine that uses an unresolved precision symbol is
+    rejected. We test when the precision symbol appears in an executable
+    statement and when it appears in a constant initialisation.'''
+    code = (
+        f"module test_mod\n"
+        f"  use kinds_mod\n"
+        f"contains\n"
+        f"  subroutine run_it()\n"
+        f"    use a_mod\n"
+        f"    integer :: i\n"
+        f"    i = 10_i_def\n"
+        f"    call sub(i)\n"
+        f"  end subroutine run_it\n"
+        f"  subroutine sub(idx)\n"
+        f"    integer, intent(inout) :: idx\n"
+        f"    {code_body}\n"
+        f"  end subroutine sub\n"
+        f"end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    call = psyir.walk(Call)[0]
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    if "_wp" in code_body:
+        var_name = "wp"
+    else:
+        var_name = "i_def"
+    assert (f"Routine 'sub' cannot be inlined because it accesses variable "
+            f"'{var_name}' and this cannot be found in any of the containers "
+            f"directly imported into its symbol table" in str(err.value))
+
+
+def test_validate_resolved_precision_sym(fortran_reader, monkeypatch,
+                                         tmpdir):
+    '''Test that a routine that uses a resolved precision symbol is
+    rejected.'''
+    code = (
+        "module test_mod\n"
+        "  use kinds_mod\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    use a_mod\n"
+        "    integer :: i\n"
+        "    a_var = a_clash\n"
+        "    i = 10_i_def\n"
+        "    call sub(i)\n"
+        "    call sub2(i)\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub(idx)\n"
+        "    integer, intent(inout) :: idx\n"
+        "    idx = idx + 5_i_def\n"
+        "  end subroutine sub\n"
+        "  subroutine sub2(idx)\n"
+        "    use kinds_mod\n"
+        "    integer, intent(inout) :: idx\n"
+        "    idx = idx + 5_i_def\n"
+        "  end subroutine sub2\n"
+        "end module test_mod\n")
+    # Set up include_path to import the proper module
+    monkeypatch.setattr(Config.get(), '_include_paths', [str(tmpdir)])
+    filename = os.path.join(str(tmpdir), "kinds_mod.f90")
+    with open(filename, "w", encoding='UTF-8') as module:
+        module.write('''
+        module kinds_mod
+          integer, parameter :: i_def = kind(1)
+        end module kinds_mod
+        ''')
+    psyir = fortran_reader.psyir_from_source(code)
+    inline_trans = InlineTrans()
+    # First subroutine accesses i_def from parent Container.
+    calls = psyir.walk(Call)
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(calls[0])
+    assert ("Routine 'sub' cannot be inlined because it accesses variable "
+            "'i_def' and this cannot be found in any of the containers "
+            "directly imported into its symbol table." in str(err.value))
+    # Second subroutine imports i_def directly into its own SymbolTable and
+    # so is OK to inline.
+    inline_trans.validate(calls[1])
+
+
 def test_validate_import_clash(fortran_reader):
     '''Test that validate() raises the expected error when two symbols of the
     same name are imported from different containers at the call site and
@@ -1006,6 +1790,34 @@ def test_validate_non_local_symbol(fortran_reader):
             "'trouble' from its parent container" in str(err.value))
 
 
+def test_validate_wrong_number_args(fortran_reader):
+    ''' Test that validate rejects inlining routines with different number
+    of arguments.
+    '''
+    code = (
+        "module test_mod\n"
+        "  integer :: trouble\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i, trouble)\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub(idx)\n"
+        "    integer :: idx\n"
+        "    idx = idx + 1\n"
+        "  end subroutine sub\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    call = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("Cannot inline 'call sub(i, trouble)' because the number of "
+            "arguments supplied to the call (2) does not match the number of "
+            "arguments the routine is declared to have (1)" in str(err.value))
+
+
 def test_validate_unresolved_import(fortran_reader):
     '''Test that validate rejects a routine that accesses a symbol which
     is unresolved.'''
@@ -1028,37 +1840,73 @@ def test_validate_unresolved_import(fortran_reader):
     inline_trans = InlineTrans()
     with pytest.raises(TransformationError) as err:
         inline_trans.validate(call)
-    assert ("Routine 'sub' cannot be inlined because it accesses an "
-            "un-resolved variable 'trouble'" in str(err.value))
+    assert ("Routine 'sub' cannot be inlined because it accesses variable "
+            "'trouble' and this cannot be found in any of the containers "
+            "directly imported into its symbol table." in str(err.value))
 
 
-def test_validate_array_subsection(fortran_reader):
-    '''Test that the validate method rejects an attempt to inline a routine
-    if any of its arguments are array subsections.'''
+def test_validate_unresolved_array_dim(fortran_reader):
+    '''
+    Check that validate rejects a routine if it uses an unresolved Symbol
+    when defining an array dimension.
+
+    '''
+    code = (
+        "module test_mod\n"
+        "  use some_mod\n"
+        "contains\n"
+        "  subroutine run_it()\n"
+        "    integer :: i\n"
+        "    i = 10\n"
+        "    call sub(i)\n"
+        "  end subroutine run_it\n"
+        "  subroutine sub(idx)\n"
+        "    integer :: idx\n"
+        "    integer, dimension(some_size) :: var\n"
+        "    idx = idx + 2\n"
+        "  end subroutine sub\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    call = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("Routine 'sub' cannot be inlined because it accesses variable "
+            "'some_size' and this cannot be found in any of the containers "
+            "directly imported into its symbol table" in str(err.value))
+
+
+def test_validate_unknown_arg_type(fortran_reader):
+    '''Check that validate rejects a formal argument of UnknownType (since
+    we can't then correctly map any array index expressions into the call
+    site).'''
     code = (
         "module test_mod\n"
         "contains\n"
         "subroutine main\n"
-        "  REAL A(100, 100)\n"
-        "  CALL S(A(26:,2:))\n"
-        "end subroutine\n"
-        "subroutine s(x)\n"
-        "  real :: x(:, :)\n"
-        "  x(:,:) = 0.0\n"
-        "end subroutine\n"
-        "end module\n")
+        "  real, target :: var = 0.0\n"
+        "  real, pointer :: ptr => null()\n"
+        "  ptr => var\n"
+        "  call sub(ptr)\n"
+        "end subroutine main\n"
+        "subroutine sub(x)\n"
+        "  real, pointer, intent(inout) :: x\n"
+        "  x = x + 1.0\n"
+        "end subroutine sub\n"
+        "end module test_mod\n"
+    )
     psyir = fortran_reader.psyir_from_source(code)
     call = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
     with pytest.raises(TransformationError) as err:
-        inline_trans.apply(call)
-    assert ("Cannot inline routine 's' because argument 'a(26:,2:)' is an "
-            "array subsection (TODO #924)" in str(err.value))
+        inline_trans.validate(call)
+    assert ("Routine 'sub' cannot be inlined because formal argument 'x' is "
+            "of UnknownType" in str(err.value))
 
 
-def test_validate_assumed_shape(fortran_reader):
+def test_validate_array_reshape(fortran_reader):
     '''Test that the validate method rejects an attempt to inline a routine
-    if any of its dummy arguments are declared to be a different shape from
+    if any of its formal arguments are declared to be a different shape from
     those at the call site.'''
     code = (
         "module test_mod\n"
@@ -1080,18 +1928,100 @@ def test_validate_assumed_shape(fortran_reader):
     call = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
     with pytest.raises(TransformationError) as err:
-        inline_trans.apply(call)
+        inline_trans.validate(call)
     assert ("Cannot inline routine 's' because it reshapes an argument: actual"
-            " argument 'a(:,:)' has rank 2 but the corresponding dummy "
+            " argument 'a(:,:)' has rank 2 but the corresponding formal "
             "argument, 'x', has rank 1" in str(err.value))
 
 
+def test_validate_array_arg_expression(fortran_reader):
+    '''
+    Check that validate rejects a call if an argument corresponding to
+    a formal array argument is not a simple Reference or Literal.
+
+    '''
+    code = (
+        "module test_mod\n"
+        "contains\n"
+        "subroutine main\n"
+        "  use some_mod, only: a, b\n"
+        "  CALL S(a+b,10)\n"
+        "end subroutine\n"
+        "subroutine s(x,m)\n"
+        "  integer, intent(in) :: m\n"
+        "  real :: x(m)\n"
+        "  integer :: i\n"
+        "  do i = 1, m\n"
+        "     x(i) = x(i) + m\n"
+        "  enddo\n"
+        "end subroutine\n"
+        "end module\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    call = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("The call 'call s(a + b, 10)\n' cannot be inlined because actual "
+            "argument 'a + b' corresponds to a formal argument with array "
+            "type but is not a Reference or a Literal" in str(err.value))
+
+
+def test_validate_indirect_range(fortran_reader):
+    '''Test that validate rejects an attempt to inline a call to a routine
+    with an argument constructed using an indirect slice.'''
+    code = (
+        "module test_mod\n"
+        "  integer, dimension(10) :: indices\n"
+        "contains\n"
+        "subroutine main\n"
+        "  real, dimension(10,10) :: var\n"
+        "  call sub(var(indices(:)))\n"
+        "end subroutine main\n"
+        "subroutine sub(x)\n"
+        "  real, dimension(:), intent(inout) :: x\n"
+        "  x(:) = 0.0\n"
+        "end subroutine sub\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    call = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("Cannot inline routine 'sub' because argument 'var(indices(:))' "
+            "has an array range in an indirect access" in str(err.value))
+
+
+def test_validate_non_unit_stride_slice(fortran_reader):
+    '''Test that validate rejects an attempt to inline a call to a routine
+    with an argument constructed using an array slice with non-unit stride.'''
+    code = (
+        "module test_mod\n"
+        "contains\n"
+        "subroutine main\n"
+        "  real, dimension(10,10) :: var\n"
+        "  call sub(var(1:10:2))\n"
+        "end subroutine main\n"
+        "subroutine sub(x)\n"
+        "  real, dimension(:), intent(inout) :: x\n"
+        "  x(:) = 0.0\n"
+        "end subroutine sub\n"
+        "end module test_mod\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    call = psyir.walk(Call)[0]
+    inline_trans = InlineTrans()
+    with pytest.raises(TransformationError) as err:
+        inline_trans.validate(call)
+    assert ("Cannot inline routine 'sub' because one of its arguments is an "
+            "array slice with a non-unit stride: 'var(::2)' (TODO #1646)" in
+            str(err.value))
+
+
 def test_validate_named_arg(fortran_reader):
     '''Test that the validate method rejects an attempt to inline a routine
     that has a named argument.'''
     # In reality, the routine with a named argument would almost certainly
     # use the 'present' intrinsic but, since that gives a CodeBlock that itself
-    # prevents inlining, out test example omits it.
+    # prevents inlining, our test example omits it.
     code = (
         "module test_mod\n"
         "contains\n"
@@ -1113,7 +2043,7 @@ def test_validate_named_arg(fortran_reader):
     call = psyir.walk(Call)[0]
     inline_trans = InlineTrans()
     with pytest.raises(TransformationError) as err:
-        inline_trans.apply(call)
+        inline_trans.validate(call)
     assert ("Routine 'sub' cannot be inlined because it has a named argument "
             "'opt' (TODO #924)" in str(err.value))