Repository: stfc/PSyclone

Issue:
Bug handling imported symbols when merging SymbolTables
While experimenting with the inlining functionality in #924 I discovered that a Symbol being brought into scope by a wildcard import would get renamed when merging two SymbolTables. This is clearly wrong.

Discussion:

Patch:
diff --git a/changelog b/changelog
index eb0fb31e1..a95dffc77 100644
--- a/changelog
+++ b/changelog
@@ -53,6 +53,8 @@
 	20) PR #2529 towards #1779. Generalise NemoArrayAccess2Loop
 	transformation to ArrayAccess2Loop.
 
+	21) PR #2517 for #2515. Fix bugs in the symbol_table merge method.
+
 release 2.5.0 14th of February 2024
 
 	1) PR #2199 for #2189. Fix bugs with missing maps in enter data
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index 864167b51..d1dccfe2f 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -968,8 +968,7 @@ class FortranWriter(LanguageWriter):
         except KeyError:
             internal_interface_symbol = None
         if unresolved_symbols and not (
-                symbol_table.has_wildcard_imports() or
-                internal_interface_symbol):
+                symbol_table.wildcard_imports() or internal_interface_symbol):
             symbols_txt = ", ".join(
                 ["'" + sym.name + "'" for sym in unresolved_symbols])
             raise VisitorError(
@@ -1196,10 +1195,13 @@ class FortranWriter(LanguageWriter):
                 except KeyError:
                     skip = []
                 whole_routine_scope.merge(sched_table, skip)
-
-            # Replace the symbol table
-            node.symbol_table.detach()
-            whole_routine_scope.attach(node)
+                if schedule is node:
+                    # Replace the Routine's symbol table as soon as we've
+                    # merged it into the new one. This ensures that the new
+                    # table has full information on outer scopes which is
+                    # important when merging.
+                    node.symbol_table.detach()
+                    whole_routine_scope.attach(node)
 
         # Generate module imports
         imports = ""
diff --git a/src/psyclone/psyir/nodes/call.py b/src/psyclone/psyir/nodes/call.py
index 80724581e..08c37d1fa 100644
--- a/src/psyclone/psyir/nodes/call.py
+++ b/src/psyclone/psyir/nodes/call.py
@@ -340,9 +340,9 @@ class Call(Statement, DataNode):
     @property
     def is_elemental(self):
         '''
-        :returns: whether the routine being called is elemental (provided with\
-            an input array it will apply the operation individually to each of\
-            the array elements and return an array with the results). If this \
+        :returns: whether the routine being called is elemental (provided with
+            an input array it will apply the operation individually to each of
+            the array elements and return an array with the results). If this
             information is not known then it returns None.
         :rtype: NoneType | bool
         '''
diff --git a/src/psyclone/psyir/symbols/interfaces.py b/src/psyclone/psyir/symbols/interfaces.py
index 0fb9dd8e0..19e25feb8 100644
--- a/src/psyclone/psyir/symbols/interfaces.py
+++ b/src/psyclone/psyir/symbols/interfaces.py
@@ -151,11 +151,12 @@ class ImportInterface(SymbolInterface):
     @container_symbol.setter
     def container_symbol(self, value):
         '''
-        :param value: the ContainerSymbol that imports the symbol with \
+        :param value: the ContainerSymbol that imports the symbol with
             this interface.
         :type value: :py:class:`psyclone.psyir.symbols.ContainerSymbol`
 
-        :raise TypeError: if the provided value is not a ContainerSymbol.
+        :raises TypeError: if the provided value is not a ContainerSymbol.
+
         '''
         # Avoid circular import
         # pylint: disable=import-outside-toplevel
@@ -176,6 +177,16 @@ class ImportInterface(SymbolInterface):
         return (f"Import(container='{self.container_symbol.name}"
                 f"'{orig_name_str})")
 
+    def __eq__(self, other):
+        if type(other) is not type(self):
+            return False
+        if (self.container_symbol.name.lower() !=
+                other.container_symbol.name.lower()):
+            return False
+        orig_name = self.orig_name.lower() if self.orig_name else ""
+        other_oname = other.orig_name.lower() if other.orig_name else ""
+        return orig_name == other_oname
+
     def copy(self):
         '''
         :returns: a copy of this object.
diff --git a/src/psyclone/psyir/symbols/symbol_table.py b/src/psyclone/psyir/symbols/symbol_table.py
index d7506028f..bb253970e 100644
--- a/src/psyclone/psyir/symbols/symbol_table.py
+++ b/src/psyclone/psyir/symbols/symbol_table.py
@@ -49,9 +49,9 @@ import copy
 from psyclone.configuration import Config
 from psyclone.errors import InternalError
 from psyclone.psyir.symbols import (
-    DataSymbol, ImportInterface, ContainerSymbol, DataTypeSymbol,
-    GenericInterfaceSymbol, RoutineSymbol, Symbol, SymbolError,
-    UnresolvedInterface)
+    DataSymbol, ContainerSymbol, DataTypeSymbol, GenericInterfaceSymbol,
+    ImportInterface, RoutineSymbol, Symbol, SymbolError, UnresolvedInterface)
+from psyclone.psyir.symbols.intrinsic_symbol import IntrinsicSymbol
 from psyclone.psyir.symbols.typed_symbol import TypedSymbol
 
 
@@ -614,34 +614,74 @@ class SymbolTable():
             when importing symbols from `other_table` into this table.
 
         '''
+        # pylint: disable-next=import-outside-toplevel
+        from psyclone.psyir.nodes import IntrinsicCall
+
         if not isinstance(symbols_to_skip, Iterable):
             raise TypeError(
                 f"check_for_clashes: 'symbols_to_skip' must be an instance of "
                 f"Iterable but got '{type(symbols_to_skip).__name__}'")
 
+        # Check whether there are any wildcard imports common to both tables.
+        self_imports = self.wildcard_imports()
+        other_imports = other_table.wildcard_imports()
+        shared_wildcard_imports = self_imports & other_imports
+        # Any wildcard imports that appear in one table but not in the other.
+        unique_wildcard_imports = self_imports ^ other_imports
+
         for other_sym in other_table.symbols:
             if other_sym.name not in self or other_sym in symbols_to_skip:
                 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 they are both IntrinsicSymbol then that's fine.
+            if (isinstance(this_sym, IntrinsicSymbol) and
+                    isinstance(other_sym, IntrinsicSymbol)):
+                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):
+                # Both symbols are imported. That's fine as long as they have
+                # the same import interface (are imported from the same
+                # Container and refer to the same Symbol in that Container).
+                if this_sym.interface != other_sym.interface:
                     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}'.")
+                        f"This table has an import of '{this_sym.name}' via "
+                        f"interface '{this_sym.interface}' but the supplied "
+                        f"table imports it via '{other_sym.interface}'.")
                 continue
+
+            if other_sym.is_unresolved and this_sym.is_unresolved:
+                # Both Symbols are unresolved.
+                if shared_wildcard_imports and not unique_wildcard_imports:
+                    # The tables have one or more wildcard imports in common
+                    # and no wildcard imports unique to one table. Therefore
+                    # the two symbols represent the same memory location.
+                    continue
+                if not (self_imports or other_imports):
+                    # Neither table has any wildcard imports.
+                    try:
+                        # An unresolved symbol representing an intrinisc is OK
+                        _ = IntrinsicCall.Intrinsic[this_sym.name.upper()]
+                        # Take this opportunity to specialise the symbol(s).
+                        if not isinstance(this_sym, IntrinsicSymbol):
+                            this_sym.specialise(IntrinsicSymbol)
+                        if not isinstance(other_sym, IntrinsicSymbol):
+                            other_sym.specialise(IntrinsicSymbol)
+                        continue
+                    except KeyError:
+                        pass
+                # We can't rename a symbol if we don't know its origin.
+                raise SymbolError(
+                    f"A symbol named '{this_sym.name}' is present but "
+                    f"unresolved in one or both tables.")
+
             # Can either of them be renamed?
             try:
                 self.rename_symbol(this_sym, "", dry_run=True)
@@ -715,7 +755,7 @@ class SymbolTable():
         '''
         Takes symbols from the supplied symbol table and adds them to this
         table (unless they appear in `symbols_to_skip`).
-        _add_container_symbols_from_table() must have been called
+        _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.
 
@@ -742,27 +782,54 @@ class SymbolTable():
 
             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)
+                self._handle_symbol_clash(old_sym, other_table)
+
+    def _handle_symbol_clash(self, old_sym, other_table):
+        '''
+        Adds the supplied Symbol to the current table in the presence
+        of a name clash. `check_for_clashes` MUST have been called
+        prior to this method in order to check for any unresolvable cases.
+
+        :param old_sym: the Symbol to be added to self.
+        :type old_sym: :py:class:`psyclone.psyir.symbols.Symbol`
+        :param other_table: the other table containing the symbol that we are
+                            trying to add to self.
+        :type other_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
+
+        :raises InternalError: if the supplied symbol is imported from a
+            Container that is not represented 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 self_csym:
+                return
+            raise InternalError(
+                f"Symbol '{old_sym.name}' imported from '{self_csym.name}' "
+                f"has not been updated to refer to the corresponding "
+                f"container in the current table.")
+
+        self_sym = self.lookup(old_sym.name)
+        if old_sym.is_unresolved and self_sym.is_unresolved:
+            # The clashing symbols are both unresolved so we ASSUME that
+            # check_for_clashes has previously determined that they must
+            # refer to the same thing and we don't have to do anything.
+            return
+
+        # A Symbol with the same name already exists so we attempt to rename
+        # first the one that we are adding and failing that, the existing
+        # symbol in this table.
+        new_name = self.next_available_name(
+            old_sym.name, other_table=other_table)
+        try:
+            other_table.rename_symbol(old_sym, new_name)
+            self.add(old_sym)
+        except SymbolError:
+            self.rename_symbol(self_sym, new_name)
+            self.add(old_sym)
 
     def merge(self, other_table, symbols_to_skip=()):
         '''Merges all of the symbols found in `other_table` into this
@@ -790,6 +857,7 @@ class SymbolTable():
             raise TypeError(
                 f"SymbolTable.merge() expects 'symbols_to_skip' to be an "
                 f"Iterable but got '{type(symbols_to_skip).__name__}'")
+
         try:
             self.check_for_clashes(other_table,
                                    symbols_to_skip=symbols_to_skip)
@@ -1025,6 +1093,41 @@ class SymbolTable():
         self.remove(old_symbol)
         self.add(new_symbol)
 
+    def _validate_remove_routinesymbol(self, symbol):
+        '''
+        Checks whether the supplied RoutineSymbol can be removed from this
+        table.
+
+        If the symbol is the target of a Call or is a member of an interface
+        definition then removal is not possible.
+
+        :param symbol: the Symbol to validate for removal.
+        :type symbol: :py:class:`psyclone.psyir.symbols.RoutineSymbol`
+
+        :raises ValueError: if the Symbol cannot be removed.
+
+        '''
+        # Check for Calls or GenericInterfaceSymbols that reference it.
+        # TODO #2271 - this walk will fail to find some symbols (e.g. in
+        # variable initialisation expressions or CodeBlocks).
+        # pylint: disable=import-outside-toplevel
+        from psyclone.psyir.nodes import Call
+        all_calls = self.node.walk(Call) if self.node else []
+        for call in all_calls:
+            if call.routine.symbol is symbol:
+                raise ValueError(
+                    f"Cannot remove RoutineSymbol '{symbol.name}' "
+                    f"because it is referenced by '{call.debug_string()}'")
+        # Check for any references to it within interfaces.
+        for sym in self._symbols.values():
+            if not isinstance(sym, GenericInterfaceSymbol):
+                continue
+            for rt_info in sym.routines:
+                if rt_info.symbol is symbol:
+                    raise ValueError(
+                        f"Cannot remove RoutineSymbol '{symbol.name}' "
+                        f"because it is referenced in interface '{sym.name}'")
+
     def remove(self, symbol):
         '''
         Remove the supplied symbol from the Symbol Table. This has a high
@@ -1038,16 +1141,16 @@ class SymbolTable():
         TODO #898. This method should check for any references/uses of
         the target symbol.
 
-        :param symbol: the container symbol to remove.
-        :type symbol: :py:class:`psyclone.psyir.symbols.ContainerSymbol`
+        :param symbol: the symbol to remove.
+        :type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
 
-        :raises TypeError: if the supplied symbol is not of type Symbol.
-        :raises NotImplementedError: the removal of this symbol type is not \
+        :raises TypeError: if the supplied parameter is not of type Symbol.
+        :raises NotImplementedError: the removal of this symbol type is not
                                      supported yet.
         :raises KeyError: if the supplied symbol is not in the symbol table.
-        :raises ValueError: if the supplied container symbol is referenced \
+        :raises ValueError: if the supplied container symbol is referenced
                             by one or more DataSymbols.
-        :raises InternalError: if the supplied symbol is not the same as the \
+        :raises InternalError: if the supplied symbol is not the same as the
                                entry with that name in this SymbolTable.
         '''
         if not isinstance(symbol, Symbol):
@@ -1087,24 +1190,10 @@ class SymbolTable():
                 f" {[sym.name for sym in self.symbols_imported_from(symbol)]} "
                 f"are imported from it - remove them first.")
 
+        # RoutineSymbols require special consideration as they may be the
+        # target of a Call or a member of a GenericInterfaceSymbol.
         if isinstance(symbol, RoutineSymbol):
-            # Check for Calls.
-            # pylint: disable=import-outside-toplevel
-            from psyclone.psyir.nodes import Call
-            all_calls = self.node.walk(Call) if self.node else []
-            for call in all_calls:
-                if call.routine.symbol is symbol:
-                    raise ValueError(
-                        f"Cannot remove RoutineSymbol '{symbol.name}' "
-                        f"because it is referenced by '{call.debug_string()}'")
-            # Check for any references to it within interfaces.
-            for sym in self._symbols.values():
-                if isinstance(sym, GenericInterfaceSymbol):
-                    if symbol in [rt.symbol for rt in sym.routines]:
-                        raise ValueError(
-                            f"Cannot remove RoutineSymbol '{symbol.name}' "
-                            f"because it is referenced in interface "
-                            f"'{sym.name}'")
+            self._validate_remove_routinesymbol(symbol)
 
         # If the symbol had a tag, it should be disassociated
         for tag, tagged_symbol in list(self._tags.items()):
@@ -1396,16 +1485,16 @@ class SymbolTable():
         referred external container. A single symbol to resolve can be
         specified for a more targeted import.
 
-        :param container_symbols: list of container symbols to search in \
-            order to resolve imported symbols. Defaults to all container \
+        :param container_symbols: list of container symbols to search in
+            order to resolve imported symbols. Defaults to all container
             symbols in the symbol table.
-        :type container_symbols: list of \
-            :py:class:`psyclone.psyir.symbols.ContainerSymbol`
-        :param symbol_target: If a symbol is given, this method will just \
-            resolve information for the given symbol. Otherwise it will \
+        :type container_symbols: list[
+            :py:class:`psyclone.psyir.symbols.ContainerSymbol`]
+        :param symbol_target: If a symbol is given, this method will just
+            resolve information for the given symbol. Otherwise it will
             resolve all possible symbols information. Defaults to None.
-        :type symbol_target: :py:class:`psyclone.psyir.symbols.Symbol` \
-            or NoneType
+        :type symbol_target: Optional[
+            :py:class:`psyclone.psyir.symbols.Symbol`]
 
         :raises SymbolError: if a symbol name clash is found between multiple \
             imports or an import and a local symbol.
@@ -1582,19 +1671,19 @@ 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 \
+        :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.
-        :raises ValueError: if the given symbol does not belong to this \
+        :raises ValueError: if the given symbol does not belong to this
                             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.
+        :raises KeyError: if the given variable name already exists in the
+                symbol table.
+        :raises SymbolError: if the specified Symbol is a ContainerSymbol, is
+            imported/unresolved or is a formal routine argument.
+        :raises SymbolError: if the specified Symbol is accessed within a
+            CodeBlock in the scope of this table.
         :raises SymbolError: if the symbol has a common block interface.
 
         '''
@@ -1618,6 +1707,11 @@ class SymbolTable():
                 f"(from Container '{symbol.interface.container_symbol.name}')."
             )
 
+        if symbol.is_unresolved:
+            raise SymbolError(
+                f"Cannot rename symbol '{symbol.name}' because it is "
+                f"unresolved.")
+
         if symbol.is_argument:
             raise SymbolError(
                 f"Cannot rename symbol '{symbol.name}' because it is a routine"
@@ -1668,22 +1762,24 @@ class SymbolTable():
         # Re-insert modified symbol
         self.add(symbol)
 
-    def has_wildcard_imports(self):
+    def wildcard_imports(self):
         '''
         Searches this symbol table and then up through any parent symbol
         tables for a ContainerSymbol that has a wildcard import.
 
-        :returns: True if a wildcard import is found, False otherwise.
-        :rtype: bool
+        :returns: the name(s) of containers which have wildcard imports
+            into the current scope.
+        :rtype: set[str]
 
         '''
+        wildcards = set()
         current_table = self
         while current_table:
             for sym in current_table.containersymbols:
                 if sym.wildcard_import:
-                    return True
+                    wildcards.add(sym.name)
             current_table = current_table.parent_symbol_table()
-        return False
+        return wildcards
 
     def view(self):
         '''
diff --git a/src/psyclone/psyir/transformations/inline_trans.py b/src/psyclone/psyir/transformations/inline_trans.py
index 3030d3f6e..e093615de 100644
--- a/src/psyclone/psyir/transformations/inline_trans.py
+++ b/src/psyclone/psyir/transformations/inline_trans.py
@@ -187,7 +187,11 @@ class InlineTrans(Transformation):
         # so we can merge symbol tables later if required.
         ancestor_table = node.ancestor(Routine).scope.symbol_table
         scope = node.scope
+
         # Copy the nodes from the Routine into the call site.
+        # TODO #924 - while doing this we should ensure that any References
+        # to common/shared Symbols in the inlined code are updated to point
+        # to the ones at the call site.
         if isinstance(new_stmts[-1], Return):
             # If the final statement of the routine is a return then
             # remove it from the list.


Test patch:
diff --git a/src/psyclone/tests/psyir/backend/fortran_routine_test.py b/src/psyclone/tests/psyir/backend/fortran_routine_test.py
index 6f4df2e3a..64ec54ba7 100644
--- a/src/psyclone/tests/psyir/backend/fortran_routine_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_routine_test.py
@@ -322,3 +322,34 @@ def test_fw_routine_flatten_tables(fortran_reader, fortran_writer):
     assert "real :: strummer_1" in code
     assert "integer :: joe_1" in code
     # We can't test for compilation because of the `use` statements.
+
+
+def test_fw_routine_flatten_tables_unresolved_sym(fortran_reader,
+                                                  fortran_writer):
+    '''
+    Check that the flattening process works correctly when nested scopes
+    contain the same, unresolved symbol being brought into scope at the
+    module level.
+
+    '''
+    code = ("module test\n"
+            "use some_mod\n"
+            "implicit none\n"
+            "contains\n"
+            "subroutine sub(b)\n"
+            "  real, intent(inout) :: b\n"
+            "  integer :: ii\n"
+            "  do ii = 1, 10\n"
+            "    b = ii + b\n"
+            "    call iom_put(ii)\n"
+            "  end do\n"
+            "  call iom_put(b)\n"
+            "end subroutine sub\n"
+            "end module test")
+    container = fortran_reader.psyir_from_source(code)
+    output = fortran_writer(container)
+    # We should still have the wildcard import.
+    assert "use some_mod\n" in output
+    # The calls to iom_put() should be unaffected.
+    assert "call iom_put(ii)" in output
+    assert "call iom_put(b)" in output
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_fortran_use_test.py b/src/psyclone/tests/psyir/frontend/fparser2_fortran_use_test.py
index 95cd8bdf9..8c54da9a0 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_fortran_use_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_fortran_use_test.py
@@ -39,16 +39,15 @@
 ''' Performs py.test tests on the support for use statements in the fparser2
     PSyIR front-end '''
 
-from __future__ import absolute_import
 import pytest
 from fparser.common.readfortran import FortranStringReader
 from fparser.two import Fortran2003
 from psyclone.psyGen import GenerationError
 from psyclone.psyir.frontend.fparser2 import Fparser2Reader
-from psyclone.psyir.nodes import KernelSchedule, Container
-from psyclone.psyir.symbols import ContainerSymbol, SymbolError, Symbol, \
-    DataSymbol, AutomaticInterface, INTEGER_SINGLE_TYPE, ScalarType, \
-    RoutineSymbol
+from psyclone.psyir.nodes import CodeBlock, Container, KernelSchedule
+from psyclone.psyir.symbols import (
+    ContainerSymbol, SymbolError, Symbol, DataSymbol, AutomaticInterface,
+    INTEGER_SINGLE_TYPE, ScalarType, RoutineSymbol)
 
 
 def test_use_return(fortran_reader):
@@ -226,6 +225,26 @@ def test_use_no_only_list():
     assert fake_parent.symbol_table.symbols_imported_from(some_mod) == []
 
 
+@pytest.mark.usefixtures("f2008_parser")
+def test_use_no_only_with_rename(fortran_reader):
+    '''
+    We don't currently support USE statements with a rename but no 'ONLY'.
+    '''
+    fake_parent = KernelSchedule("dummy_schedule")
+    processor = Fparser2Reader()
+    reader = FortranStringReader("use my_mod, local_name=>some_var\n")
+    fparser2spec = Fortran2003.Specification_Part(reader)
+    with pytest.raises(NotImplementedError) as err:
+        processor.process_declarations(fake_parent, fparser2spec.content, [])
+    assert "Found unsupported USE statement" in str(err)
+    code = '''\
+module my_mod
+  use other_mod, local_name=>some_var
+end module my_mod'''
+    psyir = fortran_reader.psyir_from_source(code)
+    assert isinstance(psyir.children[0], CodeBlock)
+
+
 @pytest.mark.usefixtures("f2008_parser")
 def test_broken_use(monkeypatch):
     ''' Check that we raise the expected error if we encounter an unrecognised
diff --git a/src/psyclone/tests/psyir/symbols/containersymbol_test.py b/src/psyclone/tests/psyir/symbols/containersymbol_test.py
index 10b0cf9f5..64ecaf07e 100644
--- a/src/psyclone/tests/psyir/symbols/containersymbol_test.py
+++ b/src/psyclone/tests/psyir/symbols/containersymbol_test.py
@@ -38,12 +38,12 @@
 
 ''' Perform py.test tests on the psygen.psyir.symbols.containersymbols file '''
 
-from __future__ import absolute_import
 import os
 import pytest
+
 from psyclone.psyir.symbols import SymbolError, Symbol
-from psyclone.psyir.symbols.containersymbol import ContainerSymbol, \
-    ContainerSymbolInterface, FortranModuleInterface
+from psyclone.psyir.symbols.containersymbol import (
+    ContainerSymbol, ContainerSymbolInterface, FortranModuleInterface)
 from psyclone.psyir.nodes import Container
 from psyclone.configuration import Config
 
diff --git a/src/psyclone/tests/psyir/symbols/interfaces_test.py b/src/psyclone/tests/psyir/symbols/interfaces_test.py
index 70a0010c5..55b3dba71 100644
--- a/src/psyclone/tests/psyir/symbols/interfaces_test.py
+++ b/src/psyclone/tests/psyir/symbols/interfaces_test.py
@@ -124,10 +124,11 @@ def test_importinterface():
     assert import_interface.container_symbol is container_symbol
     assert str(import_interface) == "Import(container='my_mod')"
 
-    import_interface = ImportInterface(container_symbol, orig_name="orig_name")
-    assert import_interface.container_symbol is container_symbol
-    assert str(import_interface) == ("Import(container='my_mod', "
-                                     "orig_name='orig_name')")
+    import_interface1 = ImportInterface(container_symbol,
+                                        orig_name="orig_name")
+    assert import_interface1.container_symbol is container_symbol
+    assert str(import_interface1) == ("Import(container='my_mod', "
+                                      "orig_name='orig_name')")
 
     with pytest.raises(TypeError) as info:
         _ = ImportInterface("hello")
@@ -138,6 +139,15 @@ def test_importinterface():
     assert ("ImportInterface orig_name parameter must be of type str or None, "
             "but found 'list'." in str(info.value))
 
+    # Two import interfaces are considered 'equal' if they are importing from
+    # a container of the same name, independent of case.
+    container_symbol2 = ContainerSymbol("my_MOD")
+    import_interface2 = ImportInterface(container_symbol2)
+    assert import_interface == import_interface2
+    assert import_interface != "my_mod"
+    # But they must have the same original name.
+    assert import_interface2 != import_interface1
+
 
 def test_importinterface_container_symbol_getter_setter():
     '''Test that the container_symbol getter and setter properties
diff --git a/src/psyclone/tests/psyir/symbols/symbol_table_test.py b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
index 930cd5c3b..d604cc352 100644
--- a/src/psyclone/tests/psyir/symbols/symbol_table_test.py
+++ b/src/psyclone/tests/psyir/symbols/symbol_table_test.py
@@ -36,7 +36,7 @@
 #         J. Henrichs, Bureau of Meteorology
 # -----------------------------------------------------------------------------
 
-''' Perform py.test tests on the psyclone.psyir.symbols.symboltable file '''
+''' Perform py.test tests on the psyclone.psyir.symbols.symbol_table file '''
 
 import re
 import os
@@ -503,7 +503,6 @@ def test_remove_routineymbols():
 def test_no_remove_routinesymbol_called(fortran_reader):
     '''Check that remove() refuses to remove a RoutineSymbol if it is the
     target of a Call.'''
-    # But not if they are referred to by a Call
     psyir = fortran_reader.psyir_from_source(
         '''
 module my_mod
@@ -533,7 +532,6 @@ end module my_mod
 def test_no_remove_routinesymbol_interface(fortran_reader):
     '''Check that remove() refuses to remove a RoutineSymbol if it is
     referred to in an interface.'''
-    # But not if they are referred to by a Call
     psyir = fortran_reader.psyir_from_source(
         '''
 module my_mod
@@ -698,9 +696,9 @@ def test_check_for_clashes_imports():
     table2.add(clash4)
     with pytest.raises(symbols.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))
+    assert ("This table has an import of 'dent' via interface "
+            "'Import(container='arthur')' but the supplied table imports it "
+            "via 'Import(container='Ford')'." in str(err.value))
 
 
 def test_check_for_clashes_cannot_rename():
@@ -759,6 +757,56 @@ def test_check_for_clashes_cannot_rename():
             "'NoneType'" in str(err.value))
 
 
+def test_check_for_clashes_wildcard_import():
+    '''Test check_for_clashes() in the presence of wildcard imports.'''
+    table1 = symbols.SymbolTable()
+    table2 = symbols.SymbolTable()
+    # A Symbol representing an intrinsic will be unresolved but shouldn't
+    # affect the ability to merge so long as we are certain that it cannot
+    # be imported from anywhere.
+    table1.new_symbol("random_number", symbol_type=symbols.RoutineSymbol,
+                      interface=symbols.UnresolvedInterface())
+    table2.new_symbol("random_number", symbol_type=symbols.RoutineSymbol,
+                      interface=symbols.UnresolvedInterface())
+    table1.check_for_clashes(table2)
+    table1.new_symbol("stavro", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.UnresolvedType(),
+                      interface=symbols.UnresolvedInterface())
+    table2.new_symbol("stavro", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.UnresolvedType(),
+                      interface=symbols.UnresolvedInterface())
+    # Both symbols unresolved and not an intrinsic.
+    with pytest.raises(symbols.SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("A symbol named 'stavro' is present but unresolved in one or "
+            "both tables." in str(err.value))
+    # Both symbols unresolved but no common wildcard import.
+    table1.add(symbols.ContainerSymbol("beta", wildcard_import=True))
+    with pytest.raises(symbols.SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("A symbol named 'stavro' is present but unresolved in one or "
+            "both tables." in str(err.value))
+    # Add a wildcard import to the second table but from a different container.
+    table2.add(symbols.ContainerSymbol("romula", wildcard_import=True))
+    with pytest.raises(symbols.SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("A symbol named 'stavro' is present but unresolved in one or "
+            "both tables." in str(err.value))
+    # Add a wildcard import from the same container as in the first table.
+    table2.add(symbols.ContainerSymbol("beta", wildcard_import=True))
+    # We can't safely rename the symbol because it's possible that it is being
+    # brought into scope from different containers.
+    with pytest.raises(symbols.SymbolError) as err:
+        table1.check_for_clashes(table2)
+    assert ("A symbol named 'stavro' is present but unresolved in one or "
+            "both tables." in str(err.value))
+    # Remove the wildcard import that is unique to table2.
+    table2.remove(table2.lookup("romula"))
+    # Now that we are confident that 'stavro' must come from container 'beta'
+    # in both tables we know there isn't a clash.
+    table1.check_for_clashes(table2)
+
+
 def test_table_merge():
     ''' Test the SymbolTable.merge method. '''
     table1 = symbols.SymbolTable()
@@ -794,14 +842,19 @@ def test_table_merge():
     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).
+    # second table being renamed (as that preserves any references to it)
+    # unless it is an intrinsic.
     table1 = symbols.SymbolTable()
     table2 = symbols.SymbolTable()
-    table1.add(symbols.DataSymbol("theclash", symbols.INTEGER_TYPE))
-    table2.add(symbols.DataSymbol("theclash", symbols.INTEGER_TYPE))
+    for table in [table1, table2]:
+        table.add(symbols.DataSymbol("theclash", symbols.INTEGER_TYPE))
+        table.new_symbol("random_number", symbol_type=symbols.RoutineSymbol,
+                         interface=symbols.UnresolvedInterface())
     table1.merge(table2)
-    assert len(table1._symbols) == 2
+    assert len(table1._symbols) == 3
     assert table1.lookup("theclash_1") is table2.lookup("theclash_1")
+    # The symbol representing an intrinsic should not be modified.
+    assert table1.lookup("random_number") is not table2.lookup("random_number")
     # Arguments. By default they are included in a merge.
     table3 = symbols.SymbolTable()
     arg_sym = symbols.DataSymbol("trillian", symbols.INTEGER_TYPE,
@@ -859,6 +912,82 @@ def test_merge_container_syms():
     assert "due to unresolvable name clashes." in err_txt
 
 
+def test_merge_same_routine_symbol(fortran_reader):
+    '''
+    Check that we can merge two tables containing clashing symbols that
+    refer to the same routine.
+
+    '''
+    code = '''\
+    module my_mod
+      implicit none
+    contains
+      subroutine sub1()
+        use slartibartfast, only: norway
+        call norway()
+      end subroutine sub1
+      subroutine sub2()
+        use slartibartfast, only: norway
+        call norway()
+      end subroutine sub2
+    end module my_mod'''
+    psyir = fortran_reader.psyir_from_source(code)
+    routines = psyir.walk(Routine)
+    norway = routines[0].symbol_table.lookup("norway")
+    routines[0].symbol_table.merge(routines[1].symbol_table)
+    # Since the two routines are referring to the same "norway" symbol,
+    # the member of the original table should be unchanged.
+    assert routines[0].symbol_table.lookup("norway") is norway
+
+
+def test_merge_with_use_renaming(fortran_reader):
+    '''Test that merging works when both tables contain a use of
+    a specific variable from a module but give it different local names.
+
+    '''
+    code = '''\
+subroutine A
+  use modA, only : var_a => var_b
+end subroutine A
+subroutine B
+  use modA, only : var_c => var_b
+end subroutine B
+subroutine C
+  use modA, only: var_b => other_var
+end subroutine C
+subroutine D
+  use modA, only: var_b => another_var
+end subroutine D
+'''
+    psyir = fortran_reader.psyir_from_source(code)
+    routines = psyir.walk(Routine)
+    table = routines[0].symbol_table
+    # Routine B
+    table.merge(routines[1].symbol_table)
+    cntr = table.lookup("moda")
+    imported_syms = table.symbols_imported_from(cntr)
+    assert len(imported_syms) == 2
+    asym = table.lookup("var_a")
+    csym = table.lookup("var_c")
+    assert asym in imported_syms
+    assert csym in imported_syms
+    assert asym.interface.orig_name == "var_b"
+    assert csym.interface.orig_name == "var_b"
+    # Routine C
+    table.merge(routines[2].symbol_table)
+    imported_syms = table.symbols_imported_from(cntr)
+    assert len(imported_syms) == 3
+    bsym = table.lookup("var_b")
+    assert bsym in imported_syms
+    assert bsym.interface.orig_name == "other_var"
+    # Routine D. These tables cannot be merged because the same local name
+    # is associated with a different module variable.
+    print(str(routines[3].symbol_table))
+    with pytest.raises(symbols.SymbolError) as err:
+        table.merge(routines[3].symbol_table)
+    assert "Cannot merge Symbol Table of Routine 'D'" in str(err.value)
+
+
 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.'''
@@ -911,8 +1040,10 @@ def test_add_container_symbols_from_table():
     assert bclash_in_1.name != "bclash"
 
 
-def test_add_symbols_from_table():
-    '''Test for the 'internal' _add_symbols_from_table() method.'''
+def test_add_symbols_from_table_import_error():
+    '''Test that the 'internal' _add_symbols_from_table() method raises
+    the expected error if a Container import has not been updated before
+    it is called.'''
     table1 = symbols.SymbolTable()
     table2 = symbols.SymbolTable()
     csym = symbols.ContainerSymbol("ford")
@@ -924,12 +1055,58 @@ def test_add_symbols_from_table():
     table2.add(symbols.DataSymbol("prefect", symbols.INTEGER_TYPE,
                                   interface=symbols.ImportInterface(csym2)))
     with pytest.raises(InternalError) as err:
-        table1._add_symbols_from_table(table2)
+        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_add_symbols_from_table_wildcard_import():
+    '''Test that _add_symbols_from_table() handles the case where the same
+    symbol is brought into scope via a wildcard import in both tables.
+
+    '''
+    table1 = symbols.SymbolTable()
+    table2 = symbols.SymbolTable()
+    csym = symbols.ContainerSymbol("adagio")
+    table1.add(csym)
+    csym2 = csym.copy()
+    table2.add(csym2)
+    table1.new_symbol("concierto", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.UnresolvedType(),
+                      interface=symbols.UnresolvedInterface())
+    table2.new_symbol("concierto", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.UnresolvedType(),
+                      interface=symbols.UnresolvedInterface())
+    # Since this method requires that check_for_clashes() be called first,
+    # it assumes that any unresolved symbol clashes refer to the same symbol
+    # and therefore, table1 should be left unchanged.
+    table1._add_symbols_from_table(table2)
+    assert len(table1._symbols) == 2
+    assert "adagio" in table1
+    assert "concierto" in table1
+
+
+def test_add_symbols_from_table_rename_existing():
+    '''Test that _add_symbols_from_table() will rename the symbol in the
+    current table if it can't rename the one in the other table.'''
+    table1 = symbols.SymbolTable()
+    table1.new_symbol("concierto", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.INTEGER_TYPE)
+    table2 = symbols.SymbolTable()
+    table2.new_symbol("adagio", symbol_type=symbols.ContainerSymbol)
+    table2.new_symbol("concierto", symbol_type=symbols.DataSymbol,
+                      datatype=symbols.UnresolvedType(),
+                      interface=symbols.UnresolvedInterface())
+    table1._add_symbols_from_table(table2)
+    assert len(table1._symbols) == 2
+    # The original, integer scalar symbol should have been renamed.
+    orig_sym = table1.lookup("concierto_1")
+    assert orig_sym.datatype == symbols.INTEGER_TYPE
+    new_sym = table1.lookup("concierto")
+    assert isinstance(new_sym.datatype, symbols.UnresolvedType)
+
+
 def test_swap_symbol_properties():
     ''' Test the symboltable swap_properties method '''
     # pylint: disable=too-many-statements
@@ -1204,20 +1381,20 @@ def test_lookup_with_tag_3():
                     in str(info.value))
 
 
-def test_has_wildcard_imports():
-    ''' Test the has_wildcard_imports() method. '''
+def test_wildcard_imports():
+    ''' Test the wildcard_imports() method. '''
     sched_table, container_table = create_hierarchy()
     # We have no wildcard imports initially
-    assert sched_table.has_wildcard_imports() is False
-    assert container_table.has_wildcard_imports() is False
+    assert sched_table.wildcard_imports() == set()
+    assert not container_table.wildcard_imports()
     csym = symbols.ContainerSymbol("some_mod")
     container_table.add(csym)
     # Adding a container symbol without a wildcard import has no effect
-    assert container_table.has_wildcard_imports() is False
+    assert not container_table.wildcard_imports()
     # Now give it a wildcard import
     csym.wildcard_import = True
-    assert container_table.has_wildcard_imports() is True
-    assert sched_table.has_wildcard_imports() is True
+    assert container_table.wildcard_imports() == set([csym.name])
+    assert sched_table.wildcard_imports() == set([csym.name])
 
 
 def test_view():
@@ -1589,9 +1766,9 @@ def test_copy_external_import():
     assert "my_mod" in symtab
     assert var.interface.container_symbol.name == "my_mod"
     # The symtab items should be new copies not connected to the original
-    assert symtab.lookup("a") != var
-    assert symtab.lookup("my_mod") != container
-    assert symtab.lookup("a").interface.container_symbol != container
+    assert symtab.lookup("a") is not var
+    assert symtab.lookup("my_mod") is not container
+    assert symtab.lookup("a").interface.container_symbol is not container
 
     # Copy a second imported_var with a reference to the same external
     # Container
@@ -1602,13 +1779,13 @@ def test_copy_external_import():
     assert "b" in symtab
     assert "my_mod" in symtab
     assert var2.interface.container_symbol.name == "my_mod"
-    assert symtab.lookup("b") != var2
-    assert symtab.lookup("my_mod") != container2
-    assert symtab.lookup("b").interface.container_symbol != container2
+    assert symtab.lookup("b") is not var2
+    assert symtab.lookup("my_mod") is not container2
+    assert symtab.lookup("b").interface.container_symbol is not container2
 
     # The new imported_var should reuse the available container reference
-    assert symtab.lookup("a").interface.container_symbol == \
-        symtab.lookup("b").interface.container_symbol
+    assert (symtab.lookup("a").interface.container_symbol is
+            symtab.lookup("b").interface.container_symbol)
 
     # The copy of imported_vars that already exist is supported
     var3 = symbols.DataSymbol("b", symbols.UnresolvedType(),
@@ -2535,8 +2712,9 @@ def test_resolve_imports_private_symbols(fortran_reader, tmpdir, monkeypatch):
     # name_public2 also has been imported because it is a public symbol
     assert "name_public2" in symtab
     # even though we capture that other symbols are private by default
-    assert symtab.lookup("b_mod").container.symbol_table \
-        .default_visibility == symbols.Symbol.Visibility.PRIVATE
+    ctr = symtab.lookup("b_mod").container
+    assert (ctr.symbol_table.default_visibility ==
+            symbols.Symbol.Visibility.PRIVATE)
     assert "other_private" not in symtab
 
 
diff --git a/src/psyclone/tests/psyir/transformations/inline_trans_test.py b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
index 8059a5198..cf66587a6 100644
--- a/src/psyclone/tests/psyir/transformations/inline_trans_test.py
+++ b/src/psyclone/tests/psyir/transformations/inline_trans_test.py
@@ -1260,6 +1260,42 @@ def test_validate_non_local_import(fortran_reader):
             "'trouble' from its parent container." in str(err.value))
 
 
+def test_apply_shared_routine_call(fortran_reader):
+    '''
+    Test the inlining of a routine that itself calls another routine that
+    is also called from within the scope of the call site.
+    '''
+    code = '''\
+    module my_mod
+      implicit none
+    contains
+      subroutine sub1()
+        use slartibartfast, only: norway
+        call fijord()
+        call norway()
+      end subroutine sub1
+      subroutine fijord()
+        use slartibartfast, only: norway
+        call norway()
+      end subroutine fijord
+    end module my_mod'''
+    psyir = fortran_reader.psyir_from_source(code)
+    calls = psyir.walk(Call)
+    inline_trans = InlineTrans()
+    inline_trans.apply(calls[0])
+    routines = psyir.walk(Routine)
+    # After inlining we should have two calls to norway()
+    calls = routines[0].walk(Call)
+    assert len(calls) == 2
+    # Both of these calls should refer to the 'norway' symbol in scope
+    # at the call site.
+    nsym = routines[0].symbol_table.lookup("norway")
+    for call in calls:
+        if call.routine is not nsym:
+            pytest.xfail("#924 cannot reliably update references in inlined "
+                         "code.")
+
+
 def test_apply_function(fortran_reader, fortran_writer, tmpdir):
     '''Check that the apply() method works correctly for a simple call to
     a function.
@@ -1713,7 +1749,6 @@ def test_validate_unresolved_precision_sym(fortran_reader, code_body):
         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"
@@ -1739,16 +1774,14 @@ def test_validate_unresolved_precision_sym(fortran_reader, code_body):
 
 def test_validate_resolved_precision_sym(fortran_reader, monkeypatch,
                                          tmpdir):
-    '''Test that a routine that uses a resolved precision symbol is
-    rejected.'''
+    '''Test that a routine that uses a resolved precision symbol from its
+    parent Container 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"