Repository: stfc/PSyclone (python)

Issue:
LFRic alg layer error when using PSyIR: visibility symbol
`lfric_xios_tie_axis_mod.x90` causes PSyclone to fail when using the new PSyIR algorithm parsing approach. A cut down version of the problem is below. The problem appears to be with the use of `public :: update_interface` with the backend saying that it is not declared anywhere.

```
module test
  abstract interface
     subroutine update_interface()
     end subroutine update_interface
  end interface
  public :: update_interface
contains
  subroutine alg()
    use constants_mod,     only: r_def
    use field_mod,         only: field_type
    type(field_type)       :: a
    real(r_def)            :: x
    call invoke(inc_a_times_x(x,a))
  end subroutine alg
end module test
```

Pull request description:
add support for visibility symbols and abstract interfaces (closes #2213)
psyclone would not allow a visibility symbol to be created and written if its declaration was hidden in a abstract interface. This is an issue in one of the LFRic algorithm files.

Patch:
diff --git a/changelog b/changelog
index e447e9f16..5acb926d6 100644
--- a/changelog
+++ b/changelog
@@ -534,6 +534,10 @@
 	into is_constant (bool) and initial_value (PSyIR Node). This allows
 	parsing Fortran declarations with initial_value that are not constant.
 
+	181) PR #2231 for #2213. The Fortran frontend and backend now also
+	consider that UnresolvedInterface symbols can come from the body of
+	a Fortran Interface.
+
 release 2.3.1 17th of June 2022
 
 	1) PR #1747 for #1720. Adds support for If blocks to PSyAD.
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index a3ea65479..0863cb2ee 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -901,23 +901,23 @@ class FortranWriter(LanguageWriter):
 
         :param symbol_table: the SymbolTable instance.
         :type symbol_table: :py:class:`psyclone.psyir.symbols.SymbolTable`
-        :param bool is_module_scope: whether or not the declarations are in \
-                                     a module scoping unit. Default is False.
+        :param bool is_module_scope: whether or not the declarations are in
+            a module scoping unit. Default is False.
 
         :returns: the Fortran declarations for the table.
         :rtype: str
 
-        :raises VisitorError: if one of the symbols is a RoutineSymbol which \
-            does not have an ImportInterface or UnresolvedInterface ( \
-            representing named and unqualified imports respectively) or \
-            ModuleDefaultInterface (representing routines declared in the \
+        :raises VisitorError: if one of the symbols is a RoutineSymbol which
+            does not have an ImportInterface or UnresolvedInterface (
+            representing named and unqualified imports respectively) or
+            ModuleDefaultInterface (representing routines declared in the
             same module) or is not a Fortran intrinsic.
-        :raises VisitorError: if args_allowed is False and one or more \
+        :raises VisitorError: if args_allowed is False and one or more
             argument declarations exist in symbol_table.
-        :raises VisitorError: if there are any symbols (other than \
-            RoutineSymbols) in the supplied table that do not have an \
-            explicit declaration (UnresolvedInterface) and there are no \
-            wildcard imports.
+        :raises VisitorError: if there are any symbols (other than
+            RoutineSymbols) in the supplied table that do not have an
+            explicit declaration (UnresolvedInterface) and there are no
+            wildcard imports or unknown interfaces.
 
         '''
         # pylint: disable=too-many-branches
@@ -944,16 +944,25 @@ class FortranWriter(LanguageWriter):
                     isinstance(sym.interface, UnresolvedInterface)):
                 all_symbols.remove(sym)
 
-        # If the symbol table contain any symbols with an UnresolvedInterface
-        # interface (they are not explicitly declared), we need to check that
-        # we have at least one wildcard import which could be bringing them
-        # into this scope.
+        # If the symbol table contains any symbols with an
+        # UnresolvedInterface interface (they are not explicitly
+        # declared), we need to check that we have at least one
+        # wildcard import which could be bringing them into this
+        # scope, or an unknown interface which could be declaring
+        # them.
         unresolved_symbols = []
         for sym in all_symbols[:]:
             if isinstance(sym.interface, UnresolvedInterface):
                 unresolved_symbols.append(sym)
                 all_symbols.remove(sym)
-        if unresolved_symbols and not symbol_table.has_wildcard_imports():
+        try:
+            internal_interface_symbol = symbol_table.lookup(
+                "_psyclone_internal_interface")
+        except KeyError:
+            internal_interface_symbol = None
+        if unresolved_symbols and not (
+                symbol_table.has_wildcard_imports() or
+                internal_interface_symbol):
             symbols_txt = ", ".join(
                 ["'" + sym.name + "'" for sym in unresolved_symbols])
             raise VisitorError(
@@ -1134,7 +1143,6 @@ class FortranWriter(LanguageWriter):
                               node is empty or None.
 
         '''
-        # pylint: disable=too-many-branches
         if not node.name:
             raise VisitorError("Expected node name to have a value.")
 
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index 9a557bfa9..63af1e28e 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -222,57 +222,60 @@ def _first_type_match(nodelist, typekind):
     raise ValueError  # Type not found
 
 
-def _find_or_create_imported_symbol(location, name, scope_limit=None,
-                                    **kargs):
+def _find_or_create_unresolved_symbol(location, name, scope_limit=None,
+                                      **kargs):
     '''Returns the symbol with the name 'name' from a symbol table
     associated with this node or one of its ancestors.  If a symbol is found
     and the `symbol_type` keyword argument is supplied then the type of the
     existing symbol is compared with the specified type. If it is not already
     an instance of this type, then the symbol is specialised (in place).
 
-    If the symbol is not found and there are no ContainerSymbols with wildcard
-    imports then an exception is raised. However, if there are one or more
+    If the symbol is not found and there are no ContainerSymbols with
+    wildcard imports and no interfaces with unknown content then an
+    exception is raised. However, if there are one or more
     ContainerSymbols with wildcard imports (which could therefore be
-    bringing the symbol into scope) then a new Symbol with the
-    specified visibility but of unknown interface is created and
-    inserted in the most local SymbolTable that has such an import.
-    The scope_limit variable further limits the symbol table search so
-    that the search through ancestor nodes stops when the scope_limit node
-    is reached i.e. ancestors of the scope_limit node are not searched.
+    bringing the symbol into scope) or one or more interfaces with
+    unknown content then a new Symbol with the specified visibility
+    but of unknown interface is created and inserted in the most local
+    SymbolTable that has such an import. The scope_limit variable
+    further limits the symbol table search so that the search through
+    ancestor nodes stops when the scope_limit node is reached
+    i.e. ancestors of the scope_limit node are not searched.
 
     :param location: PSyIR node from which to operate.
     :type location: :py:class:`psyclone.psyir.nodes.Node`
     :param str name: the name of the symbol.
-    :param scope_limit: optional Node which limits the symbol \
-        search space to the symbol tables of the nodes within the \
-        given scope. If it is None (the default), the whole \
-        scope (all symbol tables in ancestor nodes) is searched \
-        otherwise ancestors of the scope_limit node are not \
+    :param scope_limit: optional Node which limits the symbol
+        search space to the symbol tables of the nodes within the
+        given scope. If it is None (the default), the whole
+        scope (all symbol tables in ancestor nodes) is searched
+        otherwise ancestors of the scope_limit node are not
         searched.
-    :type scope_limit: :py:class:`psyclone.psyir.nodes.Node` or \
+    :type scope_limit: :py:class:`psyclone.psyir.nodes.Node` or
         `NoneType`
 
     :returns: the matching symbol.
     :rtype: :py:class:`psyclone.psyir.symbols.Symbol`
 
     :raises TypeError: if the supplied scope_limit is not a Node.
-    :raises ValueError: if the supplied scope_limit node is not an \
+    :raises ValueError: if the supplied scope_limit node is not an
         ancestor of the supplied node.
-    :raises SymbolError: if no matching symbol is found and there are \
-        no ContainerSymbols from which it might be brought into scope.
+    :raises SymbolError: if no matching symbol is found and there are
+        no ContainerSymbols from which it might be brought into scope
+        or unknown interfaces which might contain its declaration.
 
     '''
     if not isinstance(location, Node):
         raise TypeError(
             f"The location argument '{location}' provided to "
-            f"_find_or_create_imported_symbol() is not of type `Node`.")
+            f"_find_or_create_unresolved_symbol() is not of type `Node`.")
 
     if scope_limit is not None:
         # Validate the supplied scope_limit
         if not isinstance(scope_limit, Node):
             raise TypeError(
                 f"The scope_limit argument '{scope_limit}' provided to "
-                f"_find_or_create_imported_symbol() is not of type `Node`.")
+                f"_find_or_create_unresolved_symbol() is not of type `Node`.")
 
         # Check that the scope_limit Node is an ancestor of this
         # Reference Node and raise an exception if not.
@@ -288,8 +291,8 @@ def _find_or_create_imported_symbol(location, name, scope_limit=None,
             # supplied node so raise an exception.
             raise ValueError(
                 f"The scope_limit node '{scope_limit}' provided to "
-                f"_find_or_create_imported_symbol() is not an ancestor of this"
-                f" node '{location}'.")
+                f"_find_or_create_unresolved_symbol() is not an ancestor of "
+                f"this node '{location}'.")
 
     # Keep a reference to the most local SymbolTable with a wildcard
     # import in case we need to create a Symbol.
@@ -341,8 +344,20 @@ def _find_or_create_imported_symbol(location, name, scope_limit=None,
         return first_symbol_table.new_symbol(
                 name, interface=UnresolvedInterface(), **kargs)
 
+    # Are there any interfaces that might be hiding the symbol declaration?
+    symbol_table = location.scope.symbol_table
+    try:
+        _ = symbol_table.lookup(
+            "_psyclone_internal_interface", scope_limit=scope_limit)
+        # There is an unknown interface so add this symbol.
+        return location.scope.symbol_table.new_symbol(
+            name, interface=UnresolvedInterface(), **kargs)
+    except KeyError:
+        pass
+
     # All requested Nodes have been checked but there has been no
-    # match and there are no wildcard imports so raise an exception.
+    # match and there are no wildcard imports or unknown interfaces so
+    # raise an exception.
     raise SymbolError(f"No Symbol found for name '{name}'.")
 
 
@@ -686,7 +701,7 @@ def _kind_find_or_create(name, symbol_table):
     except KeyError:
         # The SymbolTable does not contain an entry for this kind parameter
         # so look to see if it is imported and if not create one.
-        kind_symbol = _find_or_create_imported_symbol(
+        kind_symbol = _find_or_create_unresolved_symbol(
             symbol_table.node, lower_name,
             symbol_type=DataSymbol,
             datatype=default_integer_type(),
@@ -1720,7 +1735,7 @@ class Fparser2Reader():
                     f"other than 'type' are not yet supported.")
             type_name = str(walk(type_spec, Fortran2003.Type_Name)[0])
             # Do we already have a Symbol for this derived type?
-            type_symbol = _find_or_create_imported_symbol(parent, type_name)
+            type_symbol = _find_or_create_unresolved_symbol(parent, type_name)
             # pylint: disable=unidiomatic-typecheck
             if type(type_symbol) == Symbol:
                 # We do but we didn't know what kind of symbol it was. Create
@@ -2409,8 +2424,8 @@ class Fparser2Reader():
                         # If a suitable unqualified use statement is found then
                         # this call creates a Symbol and inserts it in the
                         # appropriate symbol table.
-                        _find_or_create_imported_symbol(parent, name,
-                                                        visibility=vis)
+                        _find_or_create_unresolved_symbol(parent, name,
+                                                          visibility=vis)
                     except SymbolError as err:
                         # Improve the error message with context-specific info
                         raise SymbolError(
@@ -2943,7 +2958,7 @@ class Fparser2Reader():
         loop_var = str(ctrl[0].items[1][0])
         variable_name = str(loop_var)
         try:
-            data_symbol = _find_or_create_imported_symbol(
+            data_symbol = _find_or_create_unresolved_symbol(
                 parent, variable_name, symbol_type=DataSymbol,
                 datatype=DeferredType())
         except SymbolError as err:
@@ -3500,7 +3515,7 @@ class Fparser2Reader():
             range_idx = 0
             for idx, child in enumerate(array.indices):
                 if isinstance(child, Range):
-                    symbol = _find_or_create_imported_symbol(
+                    symbol = _find_or_create_unresolved_symbol(
                         array, loop_vars[range_idx],
                         symbol_type=DataSymbol, datatype=DeferredType())
                     array.children[idx] = Reference(symbol)
@@ -3646,7 +3661,7 @@ class Fparser2Reader():
                                                    parent=member.parent)
             else:
                 # The array access is to a symbol of ArrayType
-                symbol = _find_or_create_imported_symbol(
+                symbol = _find_or_create_unresolved_symbol(
                     size_node, first_array.name, symbol_type=DataSymbol,
                     datatype=DeferredType())
                 new_ref = Reference(symbol)
@@ -3824,7 +3839,7 @@ class Fparser2Reader():
         # that first.
         if isinstance(node.children[0], Fortran2003.Name):
             # Base of reference is a scalar entity and must be a DataSymbol.
-            base_sym = _find_or_create_imported_symbol(
+            base_sym = _find_or_create_unresolved_symbol(
                 parent, node.children[0].string.lower(),
                 symbol_type=DataSymbol, datatype=DeferredType())
             base_indices = []
@@ -3834,7 +3849,7 @@ class Fparser2Reader():
             # Base of reference is an array access. Lookup the corresponding
             # symbol.
             part_ref = node.children[0]
-            base_sym = _find_or_create_imported_symbol(
+            base_sym = _find_or_create_unresolved_symbol(
                 parent, part_ref.children[0].string.lower(),
                 symbol_type=DataSymbol, datatype=DeferredType())
             # Processing the array-index expressions requires access to the
@@ -4140,7 +4155,7 @@ class Fparser2Reader():
         :rtype: :py:class:`psyclone.psyir.nodes.Reference`
 
         '''
-        symbol = _find_or_create_imported_symbol(parent, node.string)
+        symbol = _find_or_create_unresolved_symbol(parent, node.string)
         return Reference(symbol, parent=parent)
 
     def _parenthesis_handler(self, node, parent):
@@ -4184,7 +4199,7 @@ class Fparser2Reader():
         # We can't say for sure that the symbol we create here should be a
         # DataSymbol as fparser2 often identifies function calls as
         # part-references instead of function-references.
-        symbol = _find_or_create_imported_symbol(parent, reference_name)
+        symbol = _find_or_create_unresolved_symbol(parent, reference_name)
 
         if isinstance(symbol, RoutineSymbol):
             call_or_array = Call(symbol, parent=parent)


Test patch:
diff --git a/src/psyclone/tests/generator_test.py b/src/psyclone/tests/generator_test.py
index b21925a3b..f1a2bbbfb 100644
--- a/src/psyclone/tests/generator_test.py
+++ b/src/psyclone/tests/generator_test.py
@@ -367,8 +367,9 @@ def test_script_file_too_short():
         _, _ = generate(os.path.join(BASE_PATH, "dynamo0p3",
                                      "1_single_invoke.f90"),
                         api="dynamo0.3",
-                        script_name=os.path.join(BASE_PATH,
-                                                 "dynamo0p3", "testkern_xyz_mod.f90"))
+                        script_name=os.path.join(
+                            BASE_PATH,
+                            "dynamo0p3", "testkern_xyz_mod.f90"))
 
 
 def test_no_script_gocean():
diff --git a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py
index 7a2b2fe34..875f554be 100644
--- a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py
@@ -280,3 +280,34 @@ def test_gen_decls_static_variables(fortran_writer):
     sym.initial_value = 1
     sym.is_constant = True
     assert "parameter :: v1 = 1" in fortran_writer.gen_vardecl(sym)
+
+
+@pytest.mark.parametrize("visibility", ["public", "private"])
+def test_visibility_interface(fortran_reader, fortran_writer, visibility):
+    '''Test that PSyclone's Fortran backend successfully writes out
+    public/private clauses and symbols when the symbol's declaration
+    is hidden in an abstract interface.
+
+    '''
+    code = (
+        f"module test\n"
+        f"  abstract interface\n"
+        f"     subroutine update_interface()\n"
+        f"     end subroutine update_interface\n"
+        f"  end interface\n"
+        f"  {visibility} :: update_interface\n"
+        f"contains\n"
+        f"  subroutine alg()\n"
+        f"  end subroutine alg\n"
+        f"end module test\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    result = fortran_writer(psyir)
+    # The default visibility is PUBLIC so it is always output by
+    # the backend.
+    assert "public\n" in result
+    if visibility == "public":
+        # The generic PUBLIC visibility covers all symbols so we do
+        # not need to output "public :: update_interface".
+        assert "public :: update_interface" not in result
+    if visibility == "private":
+        assert "private :: update_interface" in result
diff --git a/src/psyclone/tests/psyir/conftest.py b/src/psyclone/tests/psyir/conftest.py
index 945b7f296..af450db4a 100644
--- a/src/psyclone/tests/psyir/conftest.py
+++ b/src/psyclone/tests/psyir/conftest.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2019-2020, Science and Technology Facilities Council.
+# Copyright (c) 2019-2023, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -32,14 +32,15 @@
 # POSSIBILITY OF SUCH DAMAGE.
 # -----------------------------------------------------------------------------
 # Author A. R. Porter, STFC Daresbury Lab
+# Modified R. W. Ford, STFC Daresbury Lab
 
 
 ''' Module which performs pytest set-up specific to the PSyIR tests. '''
 
-from __future__ import absolute_import
 import pytest
-from psyclone.psyir.symbols import DataSymbol, INTEGER_TYPE
+
 import psyclone.psyir.frontend.fparser2 as fp2
+from psyclone.psyir.symbols import DataSymbol, INTEGER_TYPE
 
 
 @pytest.fixture(scope="function")
@@ -52,6 +53,6 @@ def disable_declaration_check(monkeypatch):
 
     '''
     monkeypatch.setattr(
-        fp2, "_find_or_create_imported_symbol",
+        fp2, "_find_or_create_unresolved_symbol",
         lambda _1, name, _2=None: DataSymbol(name,
                                              INTEGER_TYPE))
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py
index 3f8abbb85..adb197e5e 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_find_or_create_symbol_test.py
@@ -34,7 +34,7 @@
 # Author: A. R. Porter, STFC Daresbury Lab
 # -----------------------------------------------------------------------------
 
-''' Performs py.test tests for the _find_or_create_imported_symbol routine
+''' Performs py.test tests for the _find_or_create_unresolved_symbol routine
 of the fparser2 frontend. '''
 
 
@@ -42,7 +42,7 @@ import os
 import pytest
 from fparser.common.readfortran import FortranFileReader
 from psyclone.psyGen import PSyFactory, Kern
-from psyclone.psyir.frontend.fparser2 import _find_or_create_imported_symbol
+from psyclone.psyir.frontend.fparser2 import _find_or_create_unresolved_symbol
 from psyclone.psyir.nodes import Reference, Container, Assignment, Literal, \
     KernelSchedule, BinaryOperation
 from psyclone.psyir.symbols import Symbol, DataSymbol, SymbolError, \
@@ -51,7 +51,7 @@ from psyclone.psyir.symbols import Symbol, DataSymbol, SymbolError, \
 from psyclone.tests.utilities import get_invoke
 
 
-def test_find_or_create_imported_symbol():
+def test_find_or_create_unresolved_symbol():
     '''Test that the find_or_create_imported_symbol method in a Node instance
     returns the associated symbol if there is one and raises an exception if
     not. Also test for an incorrect scope argument.'''
@@ -70,7 +70,7 @@ def test_find_or_create_imported_symbol():
     assert field_old.symbol in kernel_schedule.symbol_table.symbols
 
     # Symbol in KernelSchedule SymbolTable with KernelSchedule scope
-    assert isinstance(_find_or_create_imported_symbol(
+    assert isinstance(_find_or_create_unresolved_symbol(
         field_old, field_old.name, scope_limit=kernel_schedule), DataSymbol)
     assert field_old.symbol.name == field_old.name
 
@@ -78,66 +78,66 @@ def test_find_or_create_imported_symbol():
     # the symbol should not be found as we limit the scope to the
     # immediate parent of the reference
     with pytest.raises(SymbolError) as excinfo:
-        _ = _find_or_create_imported_symbol(field_old, field_old.name,
-                                            scope_limit=field_old.parent)
+        _ = _find_or_create_unresolved_symbol(field_old, field_old.name,
+                                              scope_limit=field_old.parent)
     assert "No Symbol found for name 'field_old'." in str(excinfo.value)
 
     # Symbol in Container SymbolTable
     alpha = references[6]
     assert alpha.name == "alpha"
-    assert isinstance(_find_or_create_imported_symbol(alpha, alpha.name),
+    assert isinstance(_find_or_create_unresolved_symbol(alpha, alpha.name),
                       DataSymbol)
     container = kernel_schedule.ancestor(Container)
     assert isinstance(container, Container)
-    assert (_find_or_create_imported_symbol(alpha, alpha.name) in
+    assert (_find_or_create_unresolved_symbol(alpha, alpha.name) in
             container.symbol_table.symbols)
 
     # Symbol in Container SymbolTable with KernelSchedule scope, so
     # the symbol should not be found as we limit the scope to the
     # kernel so do not search the container symbol table.
     with pytest.raises(SymbolError) as excinfo:
-        _ = _find_or_create_imported_symbol(alpha, alpha.name,
-                                            scope_limit=kernel_schedule)
+        _ = _find_or_create_unresolved_symbol(alpha, alpha.name,
+                                              scope_limit=kernel_schedule)
     assert "No Symbol found for name 'alpha'." in str(excinfo.value)
 
     # Symbol in Container SymbolTable with Container scope
-    assert (_find_or_create_imported_symbol(
+    assert (_find_or_create_unresolved_symbol(
         alpha, alpha.name, scope_limit=container).name == alpha.name)
 
-    # Test _find_or_create_imported_symbol with invalid location
+    # Test _find_or_create_unresolved_symbol with invalid location
     with pytest.raises(TypeError) as excinfo:
-        _ = _find_or_create_imported_symbol("hello", alpha.name)
+        _ = _find_or_create_unresolved_symbol("hello", alpha.name)
     assert ("The location argument 'hello' provided to "
-            "_find_or_create_imported_symbol() is not of type `Node`."
+            "_find_or_create_unresolved_symbol() is not of type `Node`."
             in str(excinfo.value))
 
-    # Test _find_or_create_imported_symbol with invalid scope type
+    # Test _find_or_create_unresolved_symbol with invalid scope type
     with pytest.raises(TypeError) as excinfo:
-        _ = _find_or_create_imported_symbol(alpha, alpha.name,
-                                            scope_limit="hello")
+        _ = _find_or_create_unresolved_symbol(alpha, alpha.name,
+                                              scope_limit="hello")
     assert ("The scope_limit argument 'hello' provided to "
-            "_find_or_create_imported_symbol() is not of type `Node`."
+            "_find_or_create_unresolved_symbol() is not of type `Node`."
             in str(excinfo.value))
 
     # find_or_create_symbol method with invalid scope location
     with pytest.raises(ValueError) as excinfo:
-        _ = _find_or_create_imported_symbol(alpha, alpha.name,
-                                            scope_limit=alpha)
+        _ = _find_or_create_unresolved_symbol(alpha, alpha.name,
+                                              scope_limit=alpha)
     assert ("The scope_limit node 'Reference[name:'alpha']' provided to "
-            "_find_or_create_imported_symbol() is not an ancestor of this "
+            "_find_or_create_unresolved_symbol() is not an ancestor of this "
             "node 'Reference[name:'alpha']'." in str(excinfo.value))
 
     # With a visibility parameter
-    sym = _find_or_create_imported_symbol(alpha, "very_private",
-                                          visibility=Symbol.Visibility.PRIVATE)
+    sym = _find_or_create_unresolved_symbol(
+        alpha, "very_private", visibility=Symbol.Visibility.PRIVATE)
     assert sym.name == "very_private"
     assert sym.visibility == Symbol.Visibility.PRIVATE
     assert sym is container.symbol_table.lookup("very_private",
                                                 scope_limit=container)
 
 
-def test_find_or_create_imported_symbol_2():
-    ''' Check that the _find_or_create_imported_symbol() method creates new
+def test_find_or_create_unresolved_symbol_2():
+    ''' Check that the _find_or_create_unresolved_symbol() method creates new
     symbols when appropriate. '''
     # Create some suitable PSyIR from scratch
     symbol_table = SymbolTable()
@@ -151,10 +151,10 @@ def test_find_or_create_imported_symbol_2():
     kernel1.addchild(assign)
     # We have no wildcard imports so there can be no symbol named 'undefined'
     with pytest.raises(SymbolError) as err:
-        _ = _find_or_create_imported_symbol(assign, "undefined")
+        _ = _find_or_create_unresolved_symbol(assign, "undefined")
     assert "No Symbol found for name 'undefined'" in str(err.value)
     # We should be able to find the 'tmp' symbol in the parent Container
-    sym = _find_or_create_imported_symbol(assign, "tmp")
+    sym = _find_or_create_unresolved_symbol(assign, "tmp")
     assert sym.datatype.intrinsic == ScalarType.Intrinsic.REAL
     # Add a wildcard import to the SymbolTable of the KernelSchedule
     new_container = ContainerSymbol("some_mod")
@@ -162,7 +162,7 @@ def test_find_or_create_imported_symbol_2():
     kernel1.symbol_table.add(new_container)
     # Symbol not in any container but we do have wildcard imports so we
     # get a new symbol back
-    new_symbol = _find_or_create_imported_symbol(assign, "undefined")
+    new_symbol = _find_or_create_unresolved_symbol(assign, "undefined")
     assert new_symbol.name == "undefined"
     assert isinstance(new_symbol.interface, UnresolvedInterface)
     # pylint: disable=unidiomatic-typecheck
@@ -184,12 +184,12 @@ def test_nemo_find_container_symbol(parser):
     # Get a node from the schedule
     bops = psy._invokes.invoke_list[0].schedule.walk(BinaryOperation)
     # Use it as the starting point for the search
-    symbol = _find_or_create_imported_symbol(bops[0], "alpha")
+    symbol = _find_or_create_unresolved_symbol(bops[0], "alpha")
     assert symbol.datatype.intrinsic == ScalarType.Intrinsic.REAL
 
 
 def test_find_or_create_change_symbol_type():
-    ''' Check that the _find_or_create_imported_symbol routine correctly
+    ''' Check that the _find_or_create_unresolved_symbol routine correctly
     updates the class of the located symbol if it is not an instance of
     the requested symbol type.
     '''
@@ -203,19 +203,45 @@ def test_find_or_create_change_symbol_type():
     assign = Assignment.create(Reference(tmp_sym), Literal("1.0", REAL_TYPE))
     kernel1.addchild(assign)
     # Search for the 'tmp' symbol
-    sym = _find_or_create_imported_symbol(assign, "tmp")
+    sym = _find_or_create_unresolved_symbol(assign, "tmp")
     assert sym is tmp_sym
     assert type(sym) == Symbol
     # Repeat but this time specify that we're expecting a DataSymbol
-    sym = _find_or_create_imported_symbol(assign, "tmp",
-                                          symbol_type=DataSymbol,
-                                          datatype=REAL_TYPE)
+    sym = _find_or_create_unresolved_symbol(assign, "tmp",
+                                            symbol_type=DataSymbol,
+                                            datatype=REAL_TYPE)
     assert sym is tmp_sym
     assert type(sym) == DataSymbol
     assert sym.datatype == REAL_TYPE
     # Search for 'my_sub' and specify that it should be a RoutineSymbol
-    sym2 = _find_or_create_imported_symbol(assign, "my_sub",
-                                           symbol_type=RoutineSymbol)
+    sym2 = _find_or_create_unresolved_symbol(assign, "my_sub",
+                                             symbol_type=RoutineSymbol)
     assert sym2 is sub_sym
     assert type(sym2) == RoutineSymbol
     assert isinstance(sym2.datatype, NoType)
+
+
+@pytest.mark.parametrize("visibility", [
+    Symbol.Visibility.PUBLIC, Symbol.Visibility.PRIVATE])
+def test_visibility_interface(fortran_reader, visibility):
+    '''Test that PSyclone successfully parses public/private symbols where
+    their declaration is hidden in an abstract interface. This support
+    is implemented in _find_or_create_unresolved_symbol.
+
+    '''
+    code = (
+        f"module test\n"
+        f"  abstract interface\n"
+        f"     subroutine update_interface()\n"
+        f"     end subroutine update_interface\n"
+        f"  end interface\n"
+        f"  {visibility.name} :: update_interface\n"
+        f"contains\n"
+        f"  subroutine alg()\n"
+        f"  end subroutine alg\n"
+        f"end module test\n")
+    psyir = fortran_reader.psyir_from_source(code)
+    symbol_table = psyir.children[0].symbol_table
+    assert symbol_table.lookup("_psyclone_internal_interface")
+    symbol = symbol_table.lookup("update_interface")
+    assert symbol.visibility is visibility