Repository: stfc/PSyclone

Issue:
[fortran-backend] Flattening of symbol tables does not work for use statements
In working on #1973 I discovered that if a nested scope duplicates a ContainerSymbol then that symbol is renamed when the code is generated. This is incorrect - any imports from a particular ContainerSymbol need to be merged.

Discussion:

Patch:
diff --git a/changelog b/changelog
index b2bed4059..3332c9c9f 100644
--- a/changelog
+++ b/changelog
@@ -422,6 +422,9 @@
 	141) PR #2092 for #2008. Refactors Symbol interfaces and adds support
 	for Static/SAVE variables.
 
+	142) PR #2089 for #2005. Add a SymbolTable merge method and improve symbol
+	renaming capabilities.
+
 release 2.3.1 17th of June 2022
 
 	1) PR #1747 for #1720. Adds support for If blocks to PSyAD.
diff --git a/examples/psyir/modify.py b/examples/psyir/modify.py
index 52996ffad..35b2a6981 100644
--- a/examples/psyir/modify.py
+++ b/examples/psyir/modify.py
@@ -68,7 +68,7 @@ def modify_psyir_tree():
     subroutine = container.children[0]
 
     # Rename one of the subroutine local symbols.
-    tmp_symbol = subroutine.symbol_table.lookup("psyir_tmp")
+    tmp_symbol = subroutine.symbol_table.lookup("psyir_tmp_1")
     subroutine.symbol_table.rename_symbol(tmp_symbol, "new_variable")
 
     # The type of a symbol might be unknown
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index c68d4e504..d2a922f17 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -1168,23 +1168,8 @@ class FortranWriter(LanguageWriter):
             # API-specific table here:
             whole_routine_scope = type(node.symbol_table)()
 
-            itself = node.symbol_table.lookup_with_tag("own_routine_symbol")
             for schedule in node.walk(Schedule):
-                for symbol in schedule.symbol_table.symbols[:]:
-
-                    # We don't need to add the Symbol representing this Routine
-                    # to the top level symbol table because in Fortran it is
-                    # already implicitly declared by the subroutine statement.
-                    if symbol is itself and isinstance(symbol, RoutineSymbol):
-                        continue
-
-                    try:
-                        whole_routine_scope.add(symbol)
-                    except KeyError:
-                        new_name = whole_routine_scope.next_available_name(
-                            symbol.name, other_table=schedule.symbol_table)
-                        schedule.symbol_table.rename_symbol(symbol, new_name)
-                        whole_routine_scope.add(symbol)
+                whole_routine_scope.merge(schedule.symbol_table)
 
             # Replace the symbol table
             node.symbol_table.detach()
diff --git a/src/psyclone/psyir/symbols/symbol_table.py b/src/psyclone/psyir/symbols/symbol_table.py
index 78fa47947..fa77af152 100644
--- a/src/psyclone/psyir/symbols/symbol_table.py
+++ b/src/psyclone/psyir/symbols/symbol_table.py
@@ -45,9 +45,9 @@ 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 import (
+    Symbol, DataSymbol, ImportInterface, ContainerSymbol, DataTypeSymbol,
+    RoutineSymbol, SymbolError, UnresolvedInterface)
 from psyclone.psyir.symbols.typed_symbol import TypedSymbol
 
 
@@ -545,6 +545,213 @@ class SymbolTable():
 
         self._symbols[key] = new_symbol
 
+    def check_for_clashes(self, other_table):
+        '''
+        Checks the symbols in the supplied table against those in
+        this table. If there is a name clash that cannot be resolved by
+        renaming then a SymbolError is raised.
+
+        :param other_table: the table for which to check for clashes.
+        :type other_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
+
+        :raises SymbolError: if there would be an unresolvable name clash \
+            when importing symbols from `other_table` into this table.
+
+        '''
+        for other_sym in other_table.symbols:
+            if other_sym.name not in self:
+                continue
+            # We have a name clash.
+            this_sym = self.lookup(other_sym.name)
+            # If they are both ContainerSymbols then that's OK as they refer to
+            # the same Container.
+            if (isinstance(this_sym, ContainerSymbol) and
+                    isinstance(other_sym, ContainerSymbol)):
+                continue
+            if other_sym.is_import and this_sym.is_import:
+                # Both symbols are imported. That's fine as long as they are
+                # imported from the same Container.
+                if not self._has_same_name(
+                        other_sym.interface.container_symbol,
+                        this_sym.interface.container_symbol):
+                    raise SymbolError(
+                        f"This table has an import of '{this_sym.name}' from "
+                        f"Container "
+                        f"'{this_sym.interface.container_symbol.name}' but "
+                        f"the supplied table imports it from Container "
+                        f"'{other_sym.interface.container_symbol.name}'.")
+                continue
+            # Can either of them be renamed?
+            try:
+                self.rename_symbol(this_sym, "", dry_run=True)
+            except SymbolError as err1:
+                try:
+                    other_table.rename_symbol(other_sym, "", dry_run=True)
+                except SymbolError as err2:
+                    # pylint: disable=raise-missing-from
+                    raise SymbolError(
+                        f"There is a name clash for symbol '{this_sym.name}' "
+                        f"that cannot be resolved by renaming "
+                        f"one of the instances because:\n- {err1}\n- {err2}")
+
+    def _add_container_symbols_from_table(self, other_table):
+        '''
+        Takes container symbols from the supplied symbol table and adds them to
+        this table. All references to each container symbol are also updated.
+        (This is a preliminary step to adding all symbols from other_table to
+        this table.)
+
+        :param other_table: the symbol table from which to take container \
+                            symbols.
+        :type other_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
+
+        '''
+        for csym in other_table.containersymbols:
+            if csym.name in self:
+                # We have a clash with another symbol in this table.
+                self_csym = self.lookup(csym.name)
+                if not isinstance(self_csym, ContainerSymbol):
+                    # The symbol in *this* table is not a Container so we
+                    # may be able to rename it.
+                    self.rename_symbol(
+                            self_csym,
+                            self.next_available_name(
+                                csym.name, other_table=other_table))
+                    # We can then add an import from the Container.
+                    self.add(csym)
+                else:
+                    # The symbol in *this* table is also a ContainerSymbol so
+                    # must refer to the same Container. If there is a wildcard
+                    # import from this Container then we'll need that in this
+                    # Table too.
+                    if csym.wildcard_import:
+                        self_csym.wildcard_import = True
+            else:
+                self.add(csym)
+            # We must update all references to this ContainerSymbol
+            # so that they point to the one in this table instead.
+            imported_syms = other_table.symbols_imported_from(csym)
+            for isym in imported_syms:
+                if isym.name in self:
+                    # We have a potential clash with a symbol imported
+                    # into the other table.
+                    other_sym = self.lookup(isym.name)
+                    if not other_sym.is_import:
+                        # The calling merge() method has already checked that
+                        # we don't have a clash between symbols of the same
+                        # name imported from different containers. We don't
+                        # support renaming an imported symbol but the
+                        # symbol in this table can be renamed so we do that.
+                        self.rename_symbol(
+                            other_sym,
+                            self.next_available_name(
+                                other_sym.name, other_table=other_table))
+                isym.interface = ImportInterface(self.lookup(csym.name))
+
+    def _add_symbols_from_table(self, other_table, include_arguments=True):
+        '''
+        Takes symbols from the supplied symbol table and adds them to this
+        table. _add_container_symbols_from_table() must have been called
+        before this method in order to handle any Container Symbols and update
+        those Symbols imported from them.
+
+        :param other_table: the symbol table from which to add symbols.
+        :type other_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
+        :param bool include_arguments: whether or not to include symbols that \
+                                       are routine arguments.
+
+        :raises InternalError: if an imported symbol is found that has not \
+            already been updated to refer to a Container in this table.
+
+        '''
+        if include_arguments:
+            symbols_to_skip = []
+        else:
+            symbols_to_skip = other_table.argument_list[:]
+
+        try:
+            # In the case where the 'other_table' belongs to a routine,
+            # we don't want or need the symbol representing that routine.
+            rsym = other_table.lookup_with_tag("own_routine_symbol")
+            if isinstance(rsym, RoutineSymbol):
+                # We only want to skip RoutineSymbols, not DataSymbols (which
+                # we may have if we have a Fortran function).
+                symbols_to_skip.append(rsym)
+        except KeyError:
+            pass
+
+        for old_sym in other_table.symbols:
+
+            if old_sym in symbols_to_skip or isinstance(old_sym,
+                                                        ContainerSymbol):
+                # We've dealt with Container symbols in _add_container_symbols.
+                continue
+
+            try:
+                self.add(old_sym)
+
+            except KeyError:
+                # We have a clash with a symbol in this table.
+                if old_sym.is_import:
+                    # This symbol is imported from a Container so should
+                    # already have been updated so as to be imported from the
+                    # corresponding container in this table.
+                    self_csym = self.lookup(
+                        old_sym.interface.container_symbol.name)
+                    if old_sym.interface.container_symbol is not self_csym:
+                        # pylint: disable=raise-missing-from
+                        raise InternalError(
+                            f"Symbol '{old_sym.name}' imported from "
+                            f"'{self_csym.name}' has not been updated to refer"
+                            f" to the corresponding container in the "
+                            f"current table.")
+                else:
+                    # A Symbol with the same name already exists so we rename
+                    # the one that we are adding. (We don't just create a new
+                    # Symbol because we need to preserve any References to it.)
+                    new_name = self.next_available_name(
+                        old_sym.name, other_table=other_table)
+                    other_table.rename_symbol(old_sym, new_name)
+                    self.add(old_sym)
+
+    def merge(self, other_table, include_arguments=True):
+        '''Merges all of the symbols found in `other_table` into this
+        table. Symbol objects in *either* table may be renamed in the
+        event of clashes.
+
+        If `other_table` belongs to a Routine and contains a symbol with the
+        same name as the Routine (i.e. a function in Fortran) then that symbol
+        is *not* added to this symbol table. Also, if `include_arguments` is
+        False then any Symbols representing formal routine arguments are
+        excluded.
+
+        :param other_table: the symbol table from which to add symbols.
+        :type other_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
+        :param bool include_arguments: whether to include Symbols that \
+            represent routine arguments.
+
+        :raises TypeError: if `other_table` is not a SymbolTable.
+        :raises SymbolError: if name clashes prevent the merge.
+
+        '''
+        if not isinstance(other_table, SymbolTable):
+            raise TypeError(f"SymbolTable.merge() expects a SymbolTable "
+                            f"instance but got '{type(other_table).__name__}'")
+
+        try:
+            self.check_for_clashes(other_table)
+        except SymbolError as err:
+            raise SymbolError(
+                f"Cannot merge {other_table.view()} with {self.view()} due to "
+                f"unresolvable name clashes.") from err
+
+        # Deal with any Container symbols first.
+        self._add_container_symbols_from_table(other_table)
+
+        # Copy each Symbol from the supplied table into this one, excluding
+        # ContainerSymbols and, optionally, those that represent formal args.
+        self._add_symbols_from_table(other_table, include_arguments)
+
     def swap_symbol_properties(self, symbol1, symbol2):
         '''Swaps the properties of symbol1 and symbol2 apart from the symbol
         name. Argument list positions are also updated appropriately.
@@ -1256,7 +1463,7 @@ class SymbolTable():
                 f"any of the searched containers: "
                 f"{[cont.name for cont in container_symbols]}.")
 
-    def rename_symbol(self, symbol, name):
+    def rename_symbol(self, symbol, name, dry_run=False):
         '''
         Rename the given symbol which should belong to this symbol table
         with the new name provided.
@@ -1264,6 +1471,8 @@ class SymbolTable():
         :param symbol: the symbol to be renamed.
         :type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
         :param str name: the new name.
+        :param bool dry_run: if True then only the validation checks are \
+                             performed.
 
         :raises TypeError: if the symbol is not a Symbol.
         :raises TypeError: if the name is not a str.
@@ -1271,6 +1480,10 @@ class SymbolTable():
                             symbol table.
         :raises KeyError: if the given variable name already exists in the \
                           symbol table.
+        :raises SymbolError: if the specified Symbol is a ContainerSymbol, is \
+                             imported or is a formal routine argument.
+        :raises SymbolError: if the specified Symbol is accessed within a \
+                             CodeBlock in the scope of this table.
 
         '''
         if not isinstance(symbol, Symbol):
@@ -1283,6 +1496,23 @@ class SymbolTable():
                 f"The symbol argument of rename_symbol() must belong to this "
                 f"symbol_table instance, but '{symbol}' does not.")
 
+        if isinstance(symbol, ContainerSymbol):
+            raise SymbolError(f"Cannot rename symbol '{symbol.name}' because "
+                              f"it is a ContainerSymbol.")
+
+        if symbol.is_import:
+            raise SymbolError(
+                f"Cannot rename symbol '{symbol.name}' because it is imported "
+                f"(from Container '{symbol.interface.container_symbol.name}')."
+            )
+
+        if symbol.is_argument:
+            raise SymbolError(
+                f"Cannot rename symbol '{symbol.name}' because it is a routine"
+                f" argument and as such may be named in a Call.")
+
+        # TODO #2043 - add check that Symbol is not in a Fortran Common Block.
+
         if not isinstance(name, str):
             raise TypeError(
                 f"The name argument of rename_symbol() must be a str, but"
@@ -1293,8 +1523,27 @@ class SymbolTable():
                 f"The name argument of rename_symbol() must not already exist "
                 f"in this symbol_table instance, but '{name}' does.")
 
+        old_name = self._normalize(symbol.name)
+        if self.node:
+            # pylint: disable=import-outside-toplevel
+            from psyclone.psyir.nodes import CodeBlock
+            cblocks = self.node.walk(CodeBlock)
+            for cblock in cblocks:
+                sym_names = [self._normalize(sname) for sname in
+                             cblock.get_symbol_names()]
+                if old_name in sym_names:
+                    cblk_txt = "\n".join(str(anode) for anode in
+                                         cblock.get_ast_nodes)
+                    raise SymbolError(
+                        f"Cannot rename Symbol '{symbol.name}' because it is "
+                        f"accessed in a CodeBlock:\n"
+                        f"{cblk_txt}")
+
+        if dry_run:
+            return
+
         # Delete current dictionary entry
-        del self._symbols[self._normalize(symbol.name)]
+        del self._symbols[old_name]
 
         # Rename symbol using protected access as the Symbol class should not
         # expose a name attribute setter.
diff --git a/src/psyclone/psyir/transformations/inline_trans.py b/src/psyclone/psyir/transformations/inline_trans.py
index 270997234..cb40bf7b5 100644
--- a/src/psyclone/psyir/transformations/inline_trans.py
+++ b/src/psyclone/psyir/transformations/inline_trans.py
@@ -44,11 +44,9 @@ from psyclone.psyir.nodes import (
     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,
-                                    RoutineSymbol, ImportInterface, Symbol,
-                                    ArrayType, INTEGER_TYPE, DeferredType,
-                                    UnknownType, StaticInterface,
-                                    UnknownInterface, ArgumentInterface)
+from psyclone.psyir.symbols import (
+    ArgumentInterface, ArrayType, DataSymbol, DeferredType, INTEGER_TYPE,
+    StaticInterface, Symbol, SymbolError, UnknownInterface, UnknownType)
 from psyclone.psyir.transformations.reference2arrayrange_trans import (
     Reference2ArrayRangeTrans)
 from psyclone.psyir.transformations.transformation_error import (
@@ -155,25 +153,13 @@ class InlineTrans(Transformation):
         # References that they contain.
         new_stmts = []
         refs = []
-        # Map from name of precision symbol to those Literals that use it.
-        precision_map = {}
         for child in routine.children:
             new_stmts.append(child.copy())
             refs.extend(new_stmts[-1].walk(Reference))
-            for lit in new_stmts[-1].walk(Literal):
-                if isinstance(lit.datatype.precision, DataSymbol):
-                    name = lit.datatype.precision.name
-                    if name not in precision_map:
-                        precision_map[name] = []
-                    precision_map[name].append(lit)
 
-        # Deal with any Container symbols first.
-        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 formal arguments
-        # or containers.
-        self._inline_symbols(table, routine_table, precision_map)
+        # Shallow copy the symbols from the routine into the table at the
+        # call site.
+        table.merge(routine_table, include_arguments=False)
 
         # When constructing new references to replace references to formal
         # args, we need to know whether any of the actual arguments are array
@@ -563,132 +549,6 @@ class InlineTrans(Transformation):
         # Just an array reference.
         return ArrayReference.create(actual_arg.symbol, members[0][1])
 
-    @staticmethod
-    def _inline_container_symbols(table, routine_table):
-        '''
-        Takes container symbols from the symbol table of the routine being
-        inlined and adds them to the table of the call site. All references
-        to each container symbol are also updated.
-
-        :param table: the symbol table at the call site.
-        :type table: :py:class:`psyclone.psyir.symbols.SymbolTable`
-        :param routine_table: the symbol table of the routine being inlined.
-        :type routine_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
-
-        '''
-        for csym in routine_table.containersymbols:
-            if csym.name in table:
-                # We have a clash with another symbol at the call site.
-                other_csym = table.lookup(csym.name)
-                if not isinstance(other_csym, ContainerSymbol):
-                    # The symbol at the call site is not a Container so we
-                    # can rename it.
-                    table.rename_symbol(
-                            other_csym,
-                            table.next_available_name(
-                                csym.name, other_table=routine_table))
-                    # We can then add an import from the Container.
-                    table.add(csym)
-                else:
-                    # If there is a wildcard import from this container in the
-                    # routine then we'll need that at the call site.
-                    if csym.wildcard_import:
-                        other_csym.wildcard_import = True
-            else:
-                table.add(csym)
-            # We must update all references to this ContainerSymbol
-            # so that they point to the one in the call site instead.
-            imported_syms = routine_table.symbols_imported_from(csym)
-            for isym in imported_syms:
-                if isym.name in table:
-                    # We have a potential clash with a symbol imported
-                    # into the routine.
-                    callsite_sym = table.lookup(isym.name)
-                    if not callsite_sym.is_import:
-                        # The validate() method has already checked that we
-                        # don't have a clash between symbols of the same name
-                        # imported from different containers.
-                        # We don't support renaming an imported symbol but the
-                        # symbol at the call site can be renamed so we do that.
-                        table.rename_symbol(
-                            callsite_sym,
-                            table.next_available_name(
-                                callsite_sym.name, other_table=routine_table))
-                isym.interface = ImportInterface(table.lookup(csym.name))
-
-    @staticmethod
-    def _inline_symbols(table, routine_table, precision_map):
-        '''
-        Takes symbols from the symbol table of the routine and adds
-        them to the table of the call site. Any literals that refer to
-        precision symbols are updated to refer to the appropriate symbol in
-        the table at the call site.
-
-        :param table: the symbol table at the call site.
-        :type table: :py:class:`psyclone.psyir.symbols.SymbolTable`
-        :param routine_table: the symbol table of the routine being inlined.
-        :type routine_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
-        :param precision_map: Lists of literals, indexed by the name of the \
-            precision symbol that they use.
-        :type precision_map: Dict[str, \
-            List[:py:class:`psyclone.psyir.nodes.Literal`]]
-
-        :raises InternalError: if an imported symbol is found that has not \
-            been updated to refer to a Container at the call site.
-
-        '''
-        routine_name = routine_table.node.name
-        formal_args = routine_table.argument_list
-
-        for old_sym in routine_table.symbols:
-
-            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 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)
-
-            except KeyError:
-                # We have a clash with a symbol at the call site.
-                if old_sym.is_import:
-                    # This symbol is imported from a Container so should
-                    # already have been updated so as to be imported from the
-                    # corresponding container in scope at the call site.
-                    callsite_csym = table.lookup(
-                        old_sym.interface.container_symbol.name)
-                    if old_sym.interface.container_symbol is not callsite_csym:
-                        # pylint: disable=raise-missing-from
-                        raise InternalError(
-                            f"Symbol '{old_sym.name}' imported from "
-                            f"'{callsite_csym.name}' has not been updated to "
-                            f"refer to that container at the call site.")
-                else:
-                    # A Symbol with the same name already exists so we rename
-                    # the one that we are adding.
-                    new_name = table.next_available_name(
-                        old_sym.name, other_table=routine_table)
-                    routine_table.rename_symbol(old_sym, new_name)
-                    table.add(old_sym)
-
-            # Check whether this symbol is used to specify the precision of
-            # any literals.
-            if old_name in precision_map:
-                for lit in precision_map[old_name]:
-                    # A literal is immutable so create a new one with the
-                    # updated symbol as its precision.
-                    dtype = ScalarType(lit.datatype.intrinsic, old_sym)
-                    lit.replace_with(Literal(lit.value, dtype))
-
     def validate(self, node, options=None):
         '''
         Checks that the supplied node is a valid target for inlining.
@@ -798,21 +658,12 @@ class InlineTrans(Transformation):
 
         # 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
-        routine_imports = routine_table.imported_symbols
-        routine_import_names = [sym.name for sym in routine_imports]
-        for sym in callsite_imports:
-            if sym.name in routine_import_names:
-                routine_sym = routine_table.lookup(sym.name)
-                if (routine_sym.interface.container_symbol.name !=
-                        sym.interface.container_symbol.name):
-                    raise TransformationError(
-                        f"Routine '{routine.name}' imports '{sym.name}' from "
-                        f"Container "
-                        f"'{routine_sym.interface.container_symbol.name}' but "
-                        f"the call site has an import of a symbol with the "
-                        f"same name from Container "
-                        f"'{sym.interface.container_symbol.name}'.")
+        try:
+            table.check_for_clashes(routine_table)
+        except SymbolError as err:
+            raise TransformationError(
+                f"One or more symbols from routine '{routine.name}' cannot be "
+                f"added to the table at the call site.") from err
 
         # Check for unresolved symbols or for any accessed from the Container
         # containing the target routine.


Test patch:
diff --git a/src/psyclone/tests/psyir/backend/fortran_routine_test.py b/src/psyclone/tests/psyir/backend/fortran_routine_test.py
new file mode 100644
index 000000000..d3b2d8696
--- /dev/null
+++ b/src/psyclone/tests/psyir/backend/fortran_routine_test.py
@@ -0,0 +1,310 @@
+# -----------------------------------------------------------------------------
+# BSD 3-Clause License
+#
+# Copyright (c) 2019-2023, Science and Technology Facilities Council.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+# -----------------------------------------------------------------------------
+# Author R. W. Ford, STFC Daresbury Lab
+# Modified by A. R. Porter and S. Siso, STFC Daresbury Lab
+# -----------------------------------------------------------------------------
+
+'''Performs pytest tests on the Routine node handler in the
+   psyclone.psyir.backend.fortran module.'''
+
+import pytest
+
+from psyclone.psyir.backend.visitor import VisitorError
+from psyclone.psyir import nodes, symbols
+from psyclone.tests.utilities import Compile
+
+
+def test_fw_routine(fortran_reader, fortran_writer, monkeypatch, tmpdir):
+    '''Check the FortranWriter class outputs correct code when a routine node
+    is found. Also tests that an exception is raised if routine.name does not
+    have a value.
+
+    '''
+    code = (
+        "module test\n"
+        "contains\n"
+        "subroutine tmp(a, b, c)\n"
+        "  use iso_c_binding, only : c_int\n"
+        "  real, intent(out) :: a(:)\n"
+        "  real, intent(in) :: b(:)\n"
+        "  integer, intent(in) :: c\n"
+        "  if(c > 3) then\n"
+        "  a = b/c\n"
+        "  else\n"
+        "  a = b/c\n"
+        "  endif\n"
+        "end subroutine tmp\n"
+        "end module test")
+    psyir = fortran_reader.psyir_from_source(code)
+    schedule = psyir.children[0].children[0]
+    # Generate Fortran from the PSyIR schedule
+    result = fortran_writer(schedule)
+
+    assert (
+        "subroutine tmp(a, b, c)\n"
+        "  use iso_c_binding, only : c_int\n"
+        "  real, dimension(:), intent(out) :: a\n"
+        "  real, dimension(:), intent(in) :: b\n"
+        "  integer, intent(in) :: c\n"
+        "\n"
+        "  if (c > 3) then\n"
+        "    a = b / c\n"
+        "  else\n"
+        "    a = b / c\n"
+        "  end if\n"
+        "\n"
+        "end subroutine tmp\n") in result
+    assert Compile(tmpdir).string_compiles(result)
+
+    # Add distinctly named symbols in the routine sub-scopes
+    sub_scopes = schedule.walk(nodes.Schedule)[1:]
+    sub_scopes[0].symbol_table.new_symbol(
+        "symbol1", symbol_type=symbols.DataSymbol,
+        datatype=symbols.INTEGER_TYPE)
+    sub_scopes[1].symbol_table.new_symbol(
+        "symbol2", symbol_type=symbols.DataSymbol,
+        datatype=symbols.INTEGER_TYPE)
+    # They should be promoted to the routine-scope level
+    result = fortran_writer(schedule)
+    assert (
+        "  integer, intent(in) :: c\n"
+        "  integer :: symbol1\n"
+        "  integer :: symbol2\n") in result
+    assert Compile(tmpdir).string_compiles(result)
+
+    # Add symbols that will result in name clashes to sibling scopes
+    sub_scopes = schedule.walk(nodes.Schedule)[1:]
+    sub_scopes[0].symbol_table.new_symbol(
+        "symbol2", symbol_type=symbols.DataSymbol,
+        datatype=symbols.INTEGER_TYPE)
+    sub_scopes[1].symbol_table.new_symbol(
+        "symbol1", symbol_type=symbols.DataSymbol,
+        datatype=symbols.INTEGER_TYPE)
+    # Since the scopes are siblings they are allowed the same name
+    assert "symbol1" in sub_scopes[0].symbol_table
+    assert "symbol2" in sub_scopes[0].symbol_table
+    assert "symbol1" in sub_scopes[1].symbol_table
+    assert "symbol2" in sub_scopes[1].symbol_table
+    # But the back-end will promote them to routine-scope level with different
+    # names
+    result = fortran_writer(schedule)
+    assert (
+        "  integer, intent(in) :: c\n"
+        "  integer :: symbol1\n"
+        "  integer :: symbol2\n"
+        "  integer :: symbol2_1\n"
+        "  integer :: symbol1_1\n"
+        "\n") in result
+    assert Compile(tmpdir).string_compiles(result)
+
+    monkeypatch.setattr(schedule, "_name", None)
+    with pytest.raises(VisitorError) as excinfo:
+        _ = fortran_writer(schedule)
+    assert "Expected node name to have a value." in str(excinfo.value)
+
+
+def test_fw_routine_nameclash(fortran_writer):
+    ''' Test that any name clashes are handled when merging symbol tables. '''
+    sym1 = symbols.DataSymbol("var1", symbols.INTEGER_TYPE)
+    sym2 = symbols.DataSymbol("var1", symbols.INTEGER_TYPE)
+    assign1 = nodes.Assignment.create(nodes.Reference(sym1),
+                                      nodes.Literal("1", symbols.INTEGER_TYPE))
+    assign2 = nodes.Assignment.create(nodes.Reference(sym2),
+                                      nodes.Literal("2", symbols.INTEGER_TYPE))
+    ifblock = nodes.IfBlock.create(nodes.Literal("true", symbols.BOOLEAN_TYPE),
+                                   [assign1], [assign2])
+    # Place the symbols for the two variables in the tables associated with
+    # the two branches of the IfBlock. These then represent *different*
+    # variables, despite having the same name.
+    ifblock.if_body.symbol_table.add(sym1)
+    ifblock.else_body.symbol_table.add(sym2)
+    routine = nodes.Routine.create("my_sub", symbols.SymbolTable(), [ifblock])
+    result = fortran_writer(routine)
+    assert ("  integer :: var1\n"
+            "  integer :: var1_1\n"
+            "\n"
+            "  if (.true.) then\n"
+            "    var1 = 1\n"
+            "  else\n"
+            "    var1_1 = 2\n"
+            "  end if" in result)
+    # Add a symbol to the local scope of the else that will clash with
+    # the name generated with reference to the routine scope.
+    ifblock.else_body.symbol_table.add(
+        symbols.DataSymbol("var1_1", symbols.INTEGER_TYPE))
+    result = fortran_writer(routine)
+    assert ("  integer :: var1\n"
+            "  integer :: var1_2\n"
+            "  integer :: var1_1\n"
+            "\n"
+            "  if (.true.) then\n"
+            "    var1 = 1\n"
+            "  else\n"
+            "    var1_2 = 2\n"
+            "  end if" in result)
+    # Add a symbol to the routine scope that will clash with the first name
+    # generated with reference to the else scope.
+    routine.symbol_table.add(symbols.DataSymbol("var1_2",
+                                                symbols.INTEGER_TYPE))
+    result = fortran_writer(routine)
+    assert ("  integer :: var1_2\n"
+            "  integer :: var1\n"
+            "  integer :: var1_3\n"
+            "  integer :: var1_1\n"
+            "\n"
+            "  if (.true.) then\n"
+            "    var1 = 1\n"
+            "  else\n"
+            "    var1_3 = 2\n"
+            "  end if" in result)
+
+
+def test_fw_routine_program(fortran_reader, fortran_writer, tmpdir):
+    '''Check the FortranWriter class outputs correct code when a routine node
+    is found with is_program set to True i.e. it should be output as a program.
+
+    '''
+    code = (
+        "program test\n"
+        "  real :: a\n"
+        "  a = 0.0\n"
+        "end program test")
+    psyir = fortran_reader.psyir_from_source(code)
+
+    # Generate Fortran from PSyIR
+    result = fortran_writer(psyir)
+
+    assert (
+        "program test\n"
+        "  real :: a\n\n"
+        "  a = 0.0\n\n"
+        "end program test\n" in result)
+    assert Compile(tmpdir).string_compiles(result)
+
+
+def test_fw_routine_function(fortran_reader, fortran_writer, tmpdir):
+    ''' Check that the FortranWriter outputs a function when a routine node
+    is found with return_symbol set.
+
+    '''
+    code = ("module test\n"
+            "implicit none\n"
+            "real :: a\n"
+            "contains\n"
+            "function tmp(b) result(val)\n"
+            "  real :: val\n"
+            "  real :: b\n"
+            "  val = a + b\n"
+            "end function tmp\n"
+            "end module test")
+    container = fortran_reader.psyir_from_source(code)
+    # Generate Fortran from PSyIR
+    result = fortran_writer(container)
+    assert (
+        "  contains\n"
+        "  function tmp(b) result(val)\n"
+        "    real :: b\n"
+        "    real :: val\n\n"
+        "    val = a + b\n\n"
+        "  end function tmp\n" in result)
+    assert Compile(tmpdir).string_compiles(result)
+
+
+def test_fw_routine_function_no_result(fortran_reader, fortran_writer, tmpdir):
+    ''' Check that no `result(xxx)` clause is added to the output function
+    definition if the name of the return symbol matches the name of the
+    function but has different capitalisation.
+
+    '''
+    code = ("module test\n"
+            "implicit none\n"
+            "real :: a\n"
+            "contains\n"
+            "function tmp(b)\n"
+            "  real, intent(in) :: b\n"
+            "  real :: TMP\n"
+            "  tmp = a + b\n"
+            "end function tmp\n"
+            "end module test")
+    container = fortran_reader.psyir_from_source(code)
+    # Generate Fortran from PSyIR
+    result = fortran_writer(container)
+    assert "  function tmp(b)\n" in result
+    assert Compile(tmpdir).string_compiles(result)
+
+
+def test_fw_routine_flatten_tables(fortran_reader, fortran_writer):
+    '''
+    Check that module use statements in nested symbol tables are handled
+    correctly in the presence of name clashes.
+
+    '''
+    code = ("module test\n"
+            "implicit none\n"
+            "contains\n"
+            "subroutine sub(b)\n"
+            "  use some_mod, only: joe\n"
+            "  real, intent(inout) :: b\n"
+            "  real :: the_clash, strummer\n"
+            "  integer :: ii\n"
+            "  do ii = 1, 10\n"
+            "    b = ii + b\n"
+            "  end do\n"
+            "end subroutine sub\n"
+            "end module test")
+    container = fortran_reader.psyir_from_source(code)
+    # Find the loop as its body has a symbol table.
+    loop = container.walk(nodes.Loop)[0]
+    table = loop.loop_body.symbol_table
+    # Add an import to this symbol table that will clash with symbols already
+    # declared in the routine table.
+    csym = symbols.ContainerSymbol("the_clash")
+    ssym = symbols.DataSymbol("strummer", datatype=symbols.DeferredType(),
+                              interface=symbols.ImportInterface(csym))
+    # Add a variable to this table that will clash with a Container symbol
+    # in the routine table.
+    jsym = symbols.DataSymbol("joe", symbols.INTEGER_TYPE)
+    table.add(csym)
+    table.add(ssym)
+    table.add(jsym)
+    code = fortran_writer(container)
+    # Check the resulting code has the correct module use statements.
+    assert ("  subroutine sub(b)\n"
+            "    use some_mod, only : joe\n"
+            "    use the_clash, only : strummer\n" in code)
+    # While the clashing variables have been renamed.
+    assert "real :: the_clash_1" in code
+    assert "real :: strummer_1" in code
+    assert "integer :: joe_1" in code
+    # We can't test for compilation because of the `use` statements.
diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py
index 3dd5c4fe5..635f5ddc9 100644
--- a/src/psyclone/tests/psyir/backend/fortran_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_test.py
@@ -44,13 +44,13 @@ from fparser.common.readfortran import FortranStringReader
 from psyclone.psyir.backend.visitor import VisitorError
 from psyclone.psyir.backend.fortran import gen_intent, gen_datatype, \
     FortranWriter, precedence, _validate_named_args
-from psyclone.psyir.nodes import Node, CodeBlock, Container, Literal, \
-    UnaryOperation, BinaryOperation, NaryOperation, Reference, Call, \
-    KernelSchedule, ArrayReference, ArrayOfStructuresReference, Range, \
-    StructureReference, Schedule, Routine, Return, FileContainer, \
-    Assignment, IfBlock, OMPTaskloopDirective, OMPMasterDirective, \
-    OMPParallelDirective, Loop, OMPNumTasksClause, OMPDependClause, \
-    IntrinsicCall
+from psyclone.psyir.nodes import (
+    Assignment, Node, CodeBlock, Container, Literal, UnaryOperation,
+    BinaryOperation, NaryOperation, Reference, Call, KernelSchedule,
+    ArrayReference, ArrayOfStructuresReference, Range, StructureReference,
+    Schedule, Routine, Return, FileContainer, IfBlock, OMPTaskloopDirective,
+    OMPMasterDirective, OMPParallelDirective, Loop, OMPNumTasksClause,
+    OMPDependClause, IntrinsicCall)
 from psyclone.psyir.symbols import DataSymbol, SymbolTable, ContainerSymbol, \
     ImportInterface, ArgumentInterface, UnresolvedInterface, ScalarType, \
     ArrayType, INTEGER_TYPE, REAL_TYPE, CHARACTER_TYPE, BOOLEAN_TYPE, \
@@ -1034,220 +1034,6 @@ def test_fw_container_4(fortran_writer):
         "  contains\n\n"
         "end module test\n" in fortran_writer(container))
 
-
-def test_fw_routine(fortran_reader, fortran_writer, monkeypatch, tmpdir):
-    '''Check the FortranWriter class outputs correct code when a routine node
-    is found. Also tests that an exception is raised if routine.name does not
-    have a value.
-
-    '''
-    # Generate fparser2 parse tree from Fortran code.
-    code = (
-        "module test\n"
-        "contains\n"
-        "subroutine tmp(a, b, c)\n"
-        "  use iso_c_binding, only : c_int\n"
-        "  real, intent(out) :: a(:)\n"
-        "  real, intent(in) :: b(:)\n"
-        "  integer, intent(in) :: c\n"
-        "  if(c > 3) then\n"
-        "  a = b/c\n"
-        "  else\n"
-        "  a = b/c\n"
-        "  endif\n"
-        "end subroutine tmp\n"
-        "end module test")
-    psyir = fortran_reader.psyir_from_source(code)
-    schedule = psyir.children[0].children[0]
-    # Generate Fortran from the PSyIR schedule
-    result = fortran_writer(schedule)
-
-    assert (
-        "subroutine tmp(a, b, c)\n"
-        "  use iso_c_binding, only : c_int\n"
-        "  real, dimension(:), intent(out) :: a\n"
-        "  real, dimension(:), intent(in) :: b\n"
-        "  integer, intent(in) :: c\n"
-        "\n"
-        "  if (c > 3) then\n"
-        "    a = b / c\n"
-        "  else\n"
-        "    a = b / c\n"
-        "  end if\n"
-        "\n"
-        "end subroutine tmp\n") in result
-    assert Compile(tmpdir).string_compiles(result)
-
-    # Add distinctly named symbols in the routine sub-scopes
-    sub_scopes = schedule.walk(Schedule)[1:]
-    sub_scopes[0].symbol_table.new_symbol("symbol1", symbol_type=DataSymbol,
-                                          datatype=INTEGER_TYPE)
-    sub_scopes[1].symbol_table.new_symbol("symbol2", symbol_type=DataSymbol,
-                                          datatype=INTEGER_TYPE)
-    # They should be promoted to the routine-scope level
-    result = fortran_writer(schedule)
-    assert (
-        "  integer, intent(in) :: c\n"
-        "  integer :: symbol1\n"
-        "  integer :: symbol2\n") in result
-    assert Compile(tmpdir).string_compiles(result)
-
-    # Add symbols that will result in name clashes to sibling scopes
-    sub_scopes = schedule.walk(Schedule)[1:]
-    sub_scopes[0].symbol_table.new_symbol("symbol2", symbol_type=DataSymbol,
-                                          datatype=INTEGER_TYPE)
-    sub_scopes[1].symbol_table.new_symbol("symbol1", symbol_type=DataSymbol,
-                                          datatype=INTEGER_TYPE)
-    # Since the scopes are siblings they are allowed the same name
-    assert "symbol1" in sub_scopes[0].symbol_table
-    assert "symbol2" in sub_scopes[0].symbol_table
-    assert "symbol1" in sub_scopes[1].symbol_table
-    assert "symbol2" in sub_scopes[1].symbol_table
-    # But the back-end will promote them to routine-scope level with different
-    # names
-    result = fortran_writer(schedule)
-    assert (
-        "  integer, intent(in) :: c\n"
-        "  integer :: symbol1\n"
-        "  integer :: symbol2\n"
-        "  integer :: symbol2_1\n"
-        "  integer :: symbol1_1\n"
-        "\n") in result
-    assert Compile(tmpdir).string_compiles(result)
-
-    monkeypatch.setattr(schedule, "_name", None)
-    with pytest.raises(VisitorError) as excinfo:
-        _ = fortran_writer(schedule)
-    assert "Expected node name to have a value." in str(excinfo.value)
-
-
-def test_fw_routine_nameclash(fortran_writer):
-    ''' Test that any name clashes are handled when merging symbol tables. '''
-    sym1 = DataSymbol("var1", INTEGER_TYPE)
-    sym2 = DataSymbol("var1", INTEGER_TYPE)
-    assign1 = Assignment.create(Reference(sym1), Literal("1", INTEGER_TYPE))
-    assign2 = Assignment.create(Reference(sym2), Literal("2", INTEGER_TYPE))
-    ifblock = IfBlock.create(Literal("true", BOOLEAN_TYPE),
-                             [assign1], [assign2])
-    # Place the symbols for the two variables in the tables associated with
-    # the two branches of the IfBlock. These then represent *different*
-    # variables, despite having the same name.
-    ifblock.if_body.symbol_table.add(sym1)
-    ifblock.else_body.symbol_table.add(sym2)
-    routine = Routine.create("my_sub", SymbolTable(), [ifblock])
-    result = fortran_writer(routine)
-    assert ("  integer :: var1\n"
-            "  integer :: var1_1\n"
-            "\n"
-            "  if (.true.) then\n"
-            "    var1 = 1\n"
-            "  else\n"
-            "    var1_1 = 2\n"
-            "  end if" in result)
-    # Add a symbol to the local scope of the else that will clash with
-    # the name generated with reference to the routine scope.
-    ifblock.else_body.symbol_table.add(DataSymbol("var1_1", INTEGER_TYPE))
-    result = fortran_writer(routine)
-    assert ("  integer :: var1\n"
-            "  integer :: var1_2\n"
-            "  integer :: var1_1\n"
-            "\n"
-            "  if (.true.) then\n"
-            "    var1 = 1\n"
-            "  else\n"
-            "    var1_2 = 2\n"
-            "  end if" in result)
-    # Add a symbol to the routine scope that will clash with the first name
-    # generated with reference to the else scope.
-    routine.symbol_table.add(DataSymbol("var1_2", INTEGER_TYPE))
-    result = fortran_writer(routine)
-    assert ("  integer :: var1_2\n"
-            "  integer :: var1\n"
-            "  integer :: var1_3\n"
-            "  integer :: var1_1\n"
-            "\n"
-            "  if (.true.) then\n"
-            "    var1 = 1\n"
-            "  else\n"
-            "    var1_3 = 2\n"
-            "  end if" in result)
-
-
-def test_fw_routine_program(fortran_reader, fortran_writer, tmpdir):
-    '''Check the FortranWriter class outputs correct code when a routine node
-    is found with is_program set to True i.e. it should be output as a program.
-
-    '''
-    # Generate PSyIR from Fortran code via fparser2 ast
-    code = (
-        "program test\n"
-        "  real :: a\n"
-        "  a = 0.0\n"
-        "end program test")
-    psyir = fortran_reader.psyir_from_source(code)
-
-    # Generate Fortran from PSyIR
-    result = fortran_writer(psyir)
-
-    assert (
-        "program test\n"
-        "  real :: a\n\n"
-        "  a = 0.0\n\n"
-        "end program test\n" in result)
-    assert Compile(tmpdir).string_compiles(result)
-
-
-def test_fw_routine_function(fortran_reader, fortran_writer, tmpdir):
-    ''' Check that the FortranWriter outputs a function when a routine node
-    is found with return_symbol set.
-
-    '''
-    code = ("module test\n"
-            "implicit none\n"
-            "real :: a\n"
-            "contains\n"
-            "function tmp(b) result(val)\n"
-            "  real :: val\n"
-            "  real :: b\n"
-            "  val = a + b\n"
-            "end function tmp\n"
-            "end module test")
-    container = fortran_reader.psyir_from_source(code)
-    # Generate Fortran from PSyIR
-    result = fortran_writer(container)
-    assert (
-        "  contains\n"
-        "  function tmp(b) result(val)\n"
-        "    real :: b\n"
-        "    real :: val\n\n"
-        "    val = a + b\n\n"
-        "  end function tmp\n" in result)
-    assert Compile(tmpdir).string_compiles(result)
-
-
-def test_fw_routine_function_no_result(fortran_reader, fortran_writer, tmpdir):
-    ''' Check that no `result(xxx)` clause is added to the output function
-    definition if the name of the return symbol matches the name of the
-    function but has different capitalisation.
-
-    '''
-    code = ("module test\n"
-            "implicit none\n"
-            "real :: a\n"
-            "contains\n"
-            "function tmp(b)\n"
-            "  real, intent(in) :: b\n"
-            "  real :: TMP\n"
-            "  tmp = a + b\n"
-            "end function tmp\n"
-            "end module test")
-    container = fortran_reader.psyir_from_source(code)
-    # Generate Fortran from PSyIR
-    result = fortran_writer(container)
-    assert "  function tmp(b)\n" in result
-    assert Compile(tmpdir).string_compiles(result)
-
-
 # assignment and binaryoperation (not intrinsics) are already checked
 # within previous tests
 
diff --git a/src/psyclone/tests/psyir/nodes/scoping_node_test.py b/src/psyclone/tests/psyir/nodes/scoping_node_test.py
index 42848bc84..8544d1933 100644
--- a/src/psyclone/tests/psyir/nodes/scoping_node_test.py
+++ b/src/psyclone/tests/psyir/nodes/scoping_node_test.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
@@ -36,7 +36,6 @@
 
 ''' Performs py.test tests on the ScopingNode PSyIR node. '''
 
-from __future__ import absolute_import
 import pytest
 from psyclone.psyir.nodes import (Schedule, Assignment, Reference, Container,
                                   Loop, Literal, Routine, ArrayReference)
@@ -147,8 +146,11 @@ def test_scoping_node_copy_hierarchy(fortran_writer):
     assert new_schedule[0].rhs.children[0].symbol in \
         new_schedule.symbol_table.symbols
 
-    # Add the "_new" suffix to all symbol in the copied schedule
+    # Add the "_new" suffix to all Symbols in the copied schedule
     for symbol in new_schedule.symbol_table.symbols:
+        if symbol.is_argument:
+            # Can't rename a routine argument.
+            continue
         new_schedule.symbol_table.rename_symbol(symbol, symbol.name+"_new")
 
     # An update to a symbol in the outer scope must affect both copies of the
@@ -174,11 +176,11 @@ module module
     a = b_global(i)
 
   end subroutine routine
-  subroutine routine(a_new)
-    integer, intent(inout) :: a_new
+  subroutine routine(a)
+    integer, intent(inout) :: a
     integer :: i_new
 
-    a_new = b_global(i_new)
+    a = b_global(i_new)
 
   end subroutine routine
 
diff --git a/src/psyclone/tests/psyir/symbols/symbol_table_test.py b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
index 65bc4f8e7..bb609e698 100644
--- a/src/psyclone/tests/psyir/symbols/symbol_table_test.py
+++ b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
@@ -43,8 +43,9 @@ import os
 from collections import OrderedDict
 import pytest
 from psyclone.configuration import Config
-from psyclone.psyir.nodes import Schedule, Container, KernelSchedule, \
-    Literal, Reference, Assignment, Routine
+from psyclone.psyir.nodes import (
+    CodeBlock, Container, KernelSchedule,
+    Literal, Reference, Assignment, Routine, Schedule)
 from psyclone.psyir.symbols import SymbolTable, DataSymbol, ContainerSymbol, \
     AutomaticInterface, ImportInterface, ArgumentInterface, \
     ScalarType, ArrayType, DeferredType, REAL_TYPE, INTEGER_TYPE, Symbol, \
@@ -557,6 +558,244 @@ def test_swap_symbol():
     assert symbol1 not in sym_table._symbols
 
 
+def test_check_for_clashes_imports():
+    '''Test the check_for_clashes method for two tables that import the same
+    symbol from different tables.'''
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    csym1 = ContainerSymbol("ford")
+    table1.add(csym1)
+    csym2 = ContainerSymbol("Ford")
+    table2.add(csym2)
+    clash1 = DataSymbol("Prefect", INTEGER_TYPE,
+                        interface=ImportInterface(csym1))
+    table1.add(clash1)
+    clash2 = DataSymbol("prefect", INTEGER_TYPE,
+                        interface=ImportInterface(csym2))
+    table2.add(clash2)
+    # No clash as the containers are the same, just with different
+    # capitalisation.
+    table1.check_for_clashes(table2)
+    # Now create a clash between variables that have different capitalisation.
+    csym3 = ContainerSymbol("arthur")
+    table1.add(csym3)
+    clash3 = DataSymbol("dent", INTEGER_TYPE, interface=ImportInterface(csym3))
+    table1.add(clash3)
+    clash4 = DataSymbol("DENT", INTEGER_TYPE, interface=ImportInterface(csym2))
+    table2.add(clash4)
+    with pytest.raises(SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("This table has an import of 'dent' from Container 'arthur' but "
+            "the supplied table imports it from Container 'Ford'." in
+            str(err.value))
+
+
+def test_check_for_clashes_cannot_rename():
+    '''Test the check_for_clashes() method works as expected when a name clash
+    cannot be resolved by renaming.'''
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    csym1 = ContainerSymbol("vogon")
+    table1.add(csym1)
+    table1.add(DataSymbol("slab", INTEGER_TYPE))
+    csym2 = ContainerSymbol("fleet")
+    table2.add(csym2)
+    table2.add(DataSymbol("slab", DeferredType(),
+                          interface=ImportInterface(csym2)))
+    # 'slab' in table1 can be renamed.
+    table1.check_for_clashes(table2)
+    # Add another clash where one symbol is imported and the other cannot
+    # be renamed because it is a routine argument.
+    table1.add(DataSymbol("prostetnic", INTEGER_TYPE,
+                          interface=ArgumentInterface()))
+    table2.add(DataSymbol("prostetnic", DeferredType(),
+                          interface=ImportInterface(csym2)))
+    for (tab1, tab2) in [(table1, table2), (table2, table1)]:
+        with pytest.raises(SymbolError) as err:
+            tab1.check_for_clashes(tab2)
+        assert ("for symbol 'prostetnic' that cannot be resolved by renaming "
+                "one of the instances because:" in str(err.value))
+        assert ("- PSyclone SymbolTable error: Cannot rename symbol "
+                "'prostetnic' because it is imported (from Container 'fleet')"
+                in str(err.value))
+        assert ("- PSyclone SymbolTable error: Cannot rename symbol "
+                "'prostetnic' because it is a routine argument and as such "
+                "may be named in a Call." in str(err.value))
+    # Add a clash between two symbols where neither is a Container or has an
+    # ImportInterface.
+    del table1._symbols["prostetnic"]
+    table1.add(DataSymbol("jeltz", INTEGER_TYPE,
+                          interface=ArgumentInterface()))
+    table2.add(DataSymbol("jeltz", INTEGER_TYPE,
+                          interface=ArgumentInterface()))
+    with pytest.raises(SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("Cannot rename symbol 'jeltz' because it is a routine argument "
+            "and as such may be named in a Call." in str(err.value))
+
+
+def test_table_merge():
+    ''' Test the SymbolTable.merge method. '''
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    # Argument must be a table.
+    with pytest.raises(TypeError) as err:
+        table1.merge("zaphod")
+    assert ("merge() expects a SymbolTable instance but got 'str'" in
+            str(err.value))
+    # Can merge empty tables.
+    table1.merge(table2)
+    assert not table1._symbols
+    # Simple merge.
+    table2.add(DataSymbol("beeblebrox", INTEGER_TYPE))
+    # 'Own' routine symbol excluded.
+    table2.add(RoutineSymbol("dent"), tag="own_routine_symbol")
+    # Precision symbol should be included.
+    wp_sym = DataSymbol("wp", INTEGER_TYPE, constant_value=8)
+    table2.add(wp_sym)
+    table2.add(DataSymbol("marvin", ScalarType(ScalarType.Intrinsic.REAL,
+                                               wp_sym)))
+    table1.merge(table2)
+    assert table1.lookup("beeblebrox")
+    assert "dent" not in table1
+    assert "marvin" in table1
+    assert "wp" in table1
+    # Different symbols with a name clash. This results in the Symbol in the
+    # second table being renamed (as that preserves any references to it).
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    table1.add(DataSymbol("theclash", INTEGER_TYPE))
+    table2.add(DataSymbol("theclash", INTEGER_TYPE))
+    table1.merge(table2)
+    assert len(table1._symbols) == 2
+    assert table1.lookup("theclash_1") is table2.lookup("theclash_1")
+    # Arguments. By default they are included in a merge.
+    table3 = SymbolTable()
+    arg_sym = DataSymbol("trillian", INTEGER_TYPE,
+                         interface=ArgumentInterface())
+    table3.add(arg_sym)
+    table3.specify_argument_list([arg_sym])
+    table1.merge(table3)
+    assert table1.lookup("trillian") is arg_sym
+    # Check that arguments are ignored if requested.
+    table4 = SymbolTable()
+    arg_sym2 = DataSymbol("arthur", INTEGER_TYPE,
+                          interface=ArgumentInterface())
+    table4.add(arg_sym2)
+    table4.specify_argument_list([arg_sym2])
+    table1.merge(table4, include_arguments=False)
+    assert "arthur" not in table1
+
+
+def test_merge_container_syms():
+    '''Test the merge method works as expected when the tables have
+    ContainerSymbols.
+
+    '''
+    tab1 = SymbolTable()
+    tab2 = SymbolTable()
+    csym1 = ContainerSymbol("slartibartfast")
+    tab2.add(csym1)
+    wpsym = DataSymbol("wp", INTEGER_TYPE, interface=ImportInterface(csym1))
+    tab2.add(wpsym)
+    tab1.merge(tab2)
+    assert "slartibartfast" in tab1
+    assert "wp" in tab1
+    # A second table which also imports wp as well as dp.
+    tab3 = SymbolTable()
+    csym2 = ContainerSymbol("slartibartfast")
+    tab3.add(csym2)
+    wpsym2 = DataSymbol("wp", INTEGER_TYPE, interface=ImportInterface(csym2))
+    tab3.add(wpsym2)
+    dpsym = DataSymbol("dp", INTEGER_TYPE, interface=ImportInterface(csym2))
+    tab3.add(dpsym)
+    tab1.merge(tab3)
+    wp3 = tab1.lookup("wp")
+    assert wp3.interface.container_symbol.name == "slartibartfast"
+    dp3 = tab1.lookup("dp")
+    assert dp3.interface.container_symbol.name == "slartibartfast"
+    # A third table which imports wp from a *different* container.
+    tab4 = SymbolTable()
+    csym3 = ContainerSymbol("magrathea")
+    tab4.add(csym3)
+    wpsym3 = DataSymbol("wp", INTEGER_TYPE, interface=ImportInterface(csym3))
+    tab4.add(wpsym3)
+    with pytest.raises(SymbolError) as err:
+        tab1.merge(tab4)
+    err_txt = str(err.value)
+    assert "Cannot merge Symbol Table:" in err_txt
+    assert "due to unresolvable name clashes." in err_txt
+
+
+def test_add_container_symbols_from_table():
+    '''Test that the _add_container_symbols_from_table method copies Container
+    symbols into the current table and updates any import interfaces.'''
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    csym = ContainerSymbol("ford")
+    # Put a Container symbol named 'arthur' in both tables.
+    csym2 = ContainerSymbol("arthur")
+    csym3 = csym2.copy()
+    # The one in table2 will have a wildcard import.
+    # pylint: disable=no-member
+    csym2.wildcard_import = True
+    table1.add(csym3)
+    aclash = DataSymbol("aclash", INTEGER_TYPE)
+    table1.add(aclash)
+    asym = DataSymbol("prefect", INTEGER_TYPE, interface=ImportInterface(csym))
+    bsym = DataSymbol("dent", INTEGER_TYPE, interface=ImportInterface(csym2))
+    table2.add(csym)
+    table2.add(csym2)
+    table2.add(asym)
+    table2.add(bsym)
+    # Add a ContainerSymbol that will clash with a DataSymbol in the first
+    # table.
+    cclash = ContainerSymbol("aclash")
+    # Add an import of a Symbol that will also clash with a DataSymbol in
+    # the first table. As it is imported, we can't (currently) rename it so
+    # we rename the Symbol in the first table.
+    bclash = DataSymbol("bclash", INTEGER_TYPE,
+                        interface=ImportInterface(cclash))
+    table2.add(cclash)
+    table2.add(bclash)
+    bclash_in_1 = DataSymbol("bclash", INTEGER_TYPE)
+    table1.add(bclash_in_1)
+    table1._add_container_symbols_from_table(table2)
+    assert table1.lookup("ford") is csym
+    # The 'arthur' symbol object should still be the one originally in table1.
+    # However, it should now have a wildcard import.
+    assert table1.lookup("arthur") is csym3
+    assert csym3.wildcard_import
+    # Check that the import interface for a symbol in the second table has been
+    # updated to point to the container in the first table.
+    assert table2.lookup("dent").interface.container_symbol is csym3
+    assert table1.lookup("aclash") is cclash
+    # The original symbols should still be in the table but renamed.
+    assert aclash in table1.symbols
+    assert aclash.name != "aclash"
+    assert bclash_in_1 in table1.symbols
+    assert bclash_in_1.name != "bclash"
+
+
+def test_add_symbols_from_table():
+    '''Test for the 'internal' _add_symbols_from_table() method.'''
+    table1 = SymbolTable()
+    table2 = SymbolTable()
+    csym = ContainerSymbol("ford")
+    csym2 = csym.copy()
+    table1.add(csym)
+    table2.add(csym2)
+    table1.add(DataSymbol("prefect", INTEGER_TYPE,
+                          interface=ImportInterface(csym2)))
+    table2.add(DataSymbol("prefect", INTEGER_TYPE,
+                          interface=ImportInterface(csym2)))
+    with pytest.raises(InternalError) as err:
+        table1._add_symbols_from_table(table2)
+    assert ("Symbol 'prefect' imported from 'ford' has not been updated to "
+            "refer to the corresponding container in the current table."
+            in str(err.value))
+
+
 def test_swap_symbol_properties():
     ''' Test the symboltable swap_properties method '''
     # pylint: disable=too-many-statements
@@ -1310,9 +1549,11 @@ def test_deep_copy():
                       interface=ArgumentInterface(
                           ArgumentInterface.Access.READ))
     sym2 = Symbol("symbol2", interface=ImportInterface(mod))
+    sym3 = DataSymbol("symbol3", INTEGER_TYPE)
     symtab.add(mod)
     symtab.add(sym1)
     symtab.add(sym2, tag="tag1")
+    symtab.add(sym3)
     symtab.specify_argument_list([sym1])
 
     # Create a copy and check the contents are the same
@@ -1340,9 +1581,9 @@ def test_deep_copy():
     # Add new symbols and rename symbols in both symbol tables and check
     # they are not added/renamed in the other symbol table
     symtab.add(Symbol("st1"))
-    symtab.rename_symbol(symtab.lookup("symbol1"), "a")
+    symtab.rename_symbol(symtab.lookup("symbol3"), "a")
     symtab2.add(Symbol("st2"))
-    symtab2.rename_symbol(symtab2.lookup("symbol2"), "b")
+    symtab2.rename_symbol(symtab2.lookup("symbol3"), "b")
     assert "st1" in symtab
     assert "st2" in symtab2
     assert "st2" not in symtab
@@ -1353,8 +1594,8 @@ def test_deep_copy():
     assert "b" not in symtab
     assert "symbol1" in symtab2
     assert "symbol2" in symtab
-    assert "symbol1" not in symtab
-    assert "symbol2" not in symtab2
+    assert "symbol3" not in symtab
+    assert "symbol3" not in symtab2
 
 
 def test_get_symbols():
@@ -1647,7 +1888,7 @@ def test_rename_symbol():
     affects all its references. Also check that it fails when the arguments
     are not what the method expects.'''
     # Prepare the symbol table hierarchy for the test
-    schedule_symbol_table, container_symbol_table = create_hierarchy()
+    schedule_symbol_table, _ = create_hierarchy()
     symbol = schedule_symbol_table.lookup("symbol1")
     symbol.constant_value = 3
     symbol2 = schedule_symbol_table.lookup("symbol2")
@@ -1678,32 +1919,98 @@ def test_rename_symbol():
         schedule_symbol_table.lookup("symbol1")
     assert "Could not find 'symbol1' in the Symbol Table." in str(err.value)
 
-    # Check argument conditions
+
+def test_rename_symbol_errors():
+    '''Test the various checks performed by the rename_symbol method.'''
+    table = SymbolTable()
+    symbol = DataSymbol("heart", INTEGER_TYPE)
+
     with pytest.raises(TypeError) as err:
-        schedule_symbol_table.rename_symbol("not_a_symbol", "other")
+        table.rename_symbol("not_a_symbol", "other")
     assert ("The symbol argument of rename_symbol() must be a Symbol, but "
             "found: 'str'." in str(err.value))
 
-    with pytest.raises(TypeError) as err:
-        schedule_symbol_table.rename_symbol(symbol, 3)
-    assert ("The name argument of rename_symbol() must be a str, but "
-            "found:" in str(err.value))
-
     with pytest.raises(ValueError) as err:
-        container_symbol_table.rename_symbol(symbol, "somethingelse")
+        table.rename_symbol(symbol, "somethingelse")
     assert ("The symbol argument of rename_symbol() must belong to this "
             "symbol_table instance, but " in str(err.value))
 
+    table.add(symbol)
+    with pytest.raises(TypeError) as err:
+        table.rename_symbol(symbol, 3)
+    assert ("The name argument of rename_symbol() must be a str, but "
+            "found:" in str(err.value))
+
+    # Cannot rename to something that already exists in the table.
+    table.new_symbol("array")
     with pytest.raises(KeyError) as err:
-        schedule_symbol_table.rename_symbol(symbol, "array")
+        table.rename_symbol(symbol, "array")
     assert ("The name argument of rename_symbol() must not already exist in "
             "this symbol_table instance, but 'array' does." in str(err.value))
 
     with pytest.raises(KeyError) as err:
-        schedule_symbol_table.rename_symbol(symbol, "aRRay")
+        table.rename_symbol(symbol, "aRRay")
     assert ("The name argument of rename_symbol() must not already exist in "
             "this symbol_table instance, but 'aRRay' does." in str(err.value))
 
+    # Cannot rename a Container symbol.
+    csym = ContainerSymbol("benjy")
+    table.add(csym)
+    with pytest.raises(SymbolError) as err:
+        table.rename_symbol(csym, "frankie")
+    assert ("Cannot rename symbol 'benjy' because it is a ContainerSymbol." in
+            str(err.value))
+
+    # Cannot rename an imported symbol.
+    isym = DataSymbol("mouse", DeferredType(), interface=ImportInterface(csym))
+    table.add(isym)
+    with pytest.raises(SymbolError) as err:
+        table.rename_symbol(isym, "rodent")
+    assert ("Cannot rename symbol 'mouse' because it is imported (from "
+            "Container 'benjy')" in str(err.value))
+
+    # Cannot rename a routine argument.
+    asym = DataSymbol("frankie", INTEGER_TYPE, interface=ArgumentInterface())
+    table.add(asym)
+    table.specify_argument_list([asym])
+    with pytest.raises(SymbolError) as err:
+        table.rename_symbol(asym, "rodent")
+    assert ("Cannot rename symbol 'frankie' because it is a routine argument "
+            "and as such may be named in a Call." in str(err.value))
+
+
+def test_rename_codeblock_error(fortran_reader):
+    '''Test that we refuse to rename a symbol that is referenced within a
+    CodeBlock in the associated code.'''
+    code = '''
+module gold
+  integer :: my_var, other_var
+
+contains
+
+  subroutine heart_of()
+    other_var = 1.0
+    my_var = 1.0
+
+    write(*,*) my_var
+
+  end subroutine heart_of
+
+end module gold'''
+    psyir = fortran_reader.psyir_from_source(code)
+    cont = psyir.children[0]
+    assert len(cont.walk(CodeBlock)) == 1
+    # We can rename 'other_var' because it's not accessed in the CodeBlock
+    table = cont.symbol_table
+    ovar = table.lookup("other_var")
+    table.rename_symbol(ovar, "new_name")
+    assert table.lookup("new_name") is ovar
+    # We can't rename 'my_var' because it is accessed in the CodeBlock
+    with pytest.raises(SymbolError) as err:
+        table.rename_symbol(table.lookup("my_var"), "ship")
+    assert ("Cannot rename Symbol 'my_var' because it is accessed in a "
+            "CodeBlock:\nWRITE(*, *) my_var" in str(err.value))
+
 
 def test_resolve_imports(fortran_reader, tmpdir, monkeypatch):
     ''' Tests that the SymbolTable resolve_imports method works as expected
@@ -1818,6 +2125,13 @@ def test_resolve_imports(fortran_reader, tmpdir, monkeypatch):
     assert isinstance(b_2.interface, ImportInterface)
     assert b_2.interface.container_symbol == \
            subroutine.symbol_table.lookup('b_mod')
+    # Repeat but for the case where the specified symbol is not actually
+    # referenced in the current symbol table and is brought in by a wildcard
+    # import.
+    subroutine.symbol_table.resolve_imports(
+        symbol_target=DataSymbol("not_used3", DeferredType()))
+    notused3 = subroutine.symbol_table.lookup("not_used3")
+    assert notused3.datatype == INTEGER_TYPE
     # We still haven't resolved anything about a_mod or other b_mod symbols
     assert not isinstance(a_1, DataSymbol)
     assert not isinstance(b_1, DataSymbol)
diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
index 8ef254d6c..62740aa2d 100644
--- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py
+++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
@@ -932,8 +932,8 @@ def test_apply_repeated_module_use(fortran_reader, fortran_writer):
     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")
+    # Check container symbol has not been renamed.
+    assert "use model_mod_1" not in output
     assert ("  subroutine run_it()\n"
             "    use model_mod, only : radius\n"
             "    integer :: i\n" in output)
@@ -1231,39 +1231,6 @@ def test_apply_callsite_rename_container(fortran_reader, fortran_writer):
             "    i = i * a_mod_1\n" in output)
 
 
-def test_inline_symbols_check(fortran_reader):
-    '''Test the internal consistency check within _inline_symbols.'''
-    code = (
-        "module test_mod\n"
-        "contains\n"
-        "  subroutine run_it()\n"
-        "    use a_mod, only: a_clash\n"
-        "    use kinds_mod, only: r_def\n"
-        "    integer :: i, a_var\n"
-        "    a_var = a_clash\n"
-        "    i = 10.0_r_def\n"
-        "    call sub(i)\n"
-        "    i = i * a_var\n"
-        "  end subroutine run_it\n"
-        "  subroutine sub(idx)\n"
-        "    use a_mod, only: a_clash\n"
-        "    use kinds_mod, only: i_def\n"
-        "    integer, intent(inout) :: idx\n"
-        "    idx = idx + 5_i_def + a_clash\n"
-        "  end subroutine sub\n"
-        "end module test_mod\n")
-    psyir = fortran_reader.psyir_from_source(code)
-    routines = psyir.walk(Routine)
-    caller = routines[0]
-    callee = routines[1]
-    inline_trans = InlineTrans()
-    with pytest.raises(InternalError) as err:
-        inline_trans._inline_symbols(caller.symbol_table,
-                                     callee.symbol_table, {})
-    assert ("Symbol 'a_clash' imported from 'a_mod' has not been updated to "
-            "refer to that container at the call site." in str(err.value))
-
-
 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.'''
@@ -1840,9 +1807,8 @@ def test_validate_import_clash(fortran_reader):
     inline_trans = InlineTrans()
     with pytest.raises(TransformationError) as err:
         inline_trans.validate(call)
-    assert ("Routine 'sub' imports 'trouble' from Container 'other_mod' but "
-            "the call site has an import of a symbol with the same name from "
-            "Container 'some_mod'" in str(err.value))
+    assert ("One or more symbols from routine 'sub' cannot be added to the "
+            "table at the call site." in str(err.value))
 
 
 def test_validate_non_local_symbol(fortran_reader):