Repository: stfc/PSyclone

Issue:
[psyir] Fortran backend rejects unresolved symbols
After the prompt by Rupert at the telco this morning I tried processing standard NEMO code with the branch I'm working on in #1147. Unfortunately doing this immediately showed me that the PSyIR backend won't allow any unresolved symbols (other than kind parameters). Since NEMO very rarely uses the `ONLY` clause on `USE` statements, pretty much any symbol that is accessed via a `USE` will immediately prevent the backend from generating code. Given that we don't want to be parsing code unnecessarily, I think this constraint should be weakened so that the backend only checks that there is at least one import that could be bringing an unresolved symbol into scope?

Discussion:

Patch:
diff --git a/changelog b/changelog
index ba8105d8e..9f4fcd8ab 100644
--- a/changelog
+++ b/changelog
@@ -446,6 +446,9 @@
 	144) PR #1082 for #925. Adds code generation support for
 	operates_on=domain kernels in LFRic.
 
+	145) PR #1162 for #1160. Add Fortran backend support for unresolved
+	interfaces when there is wildcard imports.
+
 release 1.9.0 20th May 2020
 
 	1) #602 for #597. Modify Node.ancestor() to optionally include
diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py
index e03cdcc1f..bf4187eef 100644
--- a/src/psyclone/psyir/backend/fortran.py
+++ b/src/psyclone/psyir/backend/fortran.py
@@ -47,8 +47,7 @@ from psyclone.psyir.frontend.fparser2 import Fparser2Reader, \
     TYPE_MAP_FROM_FORTRAN
 from psyclone.psyir.symbols import DataSymbol, ArgumentInterface, \
     ContainerSymbol, ScalarType, ArrayType, UnknownType, UnknownFortranType, \
-    SymbolTable, RoutineSymbol, LocalInterface, GlobalInterface, Symbol, \
-    TypeSymbol
+    SymbolTable, RoutineSymbol, UnresolvedInterface, Symbol, TypeSymbol
 from psyclone.psyir.nodes import UnaryOperation, BinaryOperation, Operation, \
     Routine, Reference, Literal, DataNode, CodeBlock, Member, Range, Schedule
 from psyclone.psyir.backend.visitor import PSyIRVisitor, VisitorError
@@ -601,34 +600,57 @@ class FortranWriter(PSyIRVisitor):
             which does not have a GlobalInterface or LocalInterface as this \
             is not supported by this backend.
         :raises VisitorError: if args_allowed is False and one or more \
-                              argument declarations exist in symbol_table.
-        :raises VisitorError: if any symbols representing variables (i.e. \
-            not kind parameters) without an explicit declaration or 'use' \
-            are encountered.
+            argument declarations exist in symbol_table.
+        :raises VisitorError: if there are any symbols in the supplied table \
+            that do not have an explicit declaration and there are no \
+            wildcard imports.
 
         '''
         declarations = ""
-
-        if not all([isinstance(symbol.interface, (LocalInterface,
-                                                  GlobalInterface))
-                    for symbol in symbol_table.symbols
-                    if isinstance(symbol, RoutineSymbol)]):
-            raise VisitorError(
-                "Routine symbols without a global or local interface are not "
-                "supported by the Fortran back-end.")
+        # Keep a record of whether we've already checked for any wildcard
+        # imports to save doing so repeatedly
+        wildcard_imports_checked = False
+        has_wildcard_import = False
+
+        routine_symbols = [symbol for symbol in symbol_table.symbols
+                           if isinstance(symbol, RoutineSymbol)]
+        for sym in routine_symbols:
+            if (isinstance(sym.interface, UnresolvedInterface) and
+                    sym.name.upper() not in FORTRAN_INTRINSICS):
+                if not wildcard_imports_checked:
+                    has_wildcard_import = symbol_table.has_wildcard_imports()
+                    wildcard_imports_checked = True
+                if not has_wildcard_import:
+                    raise VisitorError(
+                        "Routine symbol '{0}' does not have a GlobalInterface "
+                        "or LocalInterface, is not a Fortran intrinsic and "
+                        "there is no wildcard import which could bring it into"
+                        " scope. This is not supported by the Fortran "
+                        "back-end.".format(sym.name))
+            if isinstance(sym.interface, ArgumentInterface):
+                raise VisitorError(
+                    "Routine symbol '{0}' is passed as an argument (has an "
+                    "ArgumentInterface). This is not supported by the Fortran"
+                    " back-end.".format(sym.name))
 
         # Does the symbol table contain any symbols with a deferred
-        # interface (i.e. we don't know how they are brought into scope) that
-        # are not KIND parameters?
-        unresolved_datasymbols = symbol_table.get_unresolved_datasymbols(
-            ignore_precision=True)
+        # interface (i.e. we don't know how they are brought into scope)
+        unresolved_datasymbols = symbol_table.get_unresolved_datasymbols()
+
         if unresolved_datasymbols:
-            symbols_txt = ", ".join(
-                ["'" + sym + "'" for sym in unresolved_datasymbols])
-            raise VisitorError(
-                "The following symbols are not explicitly declared or imported"
-                " from a module (in the local scope) and are not KIND "
-                "parameters: {0}".format(symbols_txt))
+            # We do have unresolved symbols. Is there at least one wildcard
+            # import which could be bringing them into scope?
+            if not wildcard_imports_checked:
+                has_wildcard_import = symbol_table.has_wildcard_imports()
+                wildcard_imports_checked = True
+            if not has_wildcard_import:
+                symbols_txt = ", ".join(
+                    ["'" + sym + "'" for sym in unresolved_datasymbols])
+                raise VisitorError(
+                    "The following symbols are not explicitly declared or "
+                    "imported from a module and there are no wildcard imports "
+                    "which could be bringing them into scope: {0}".format(
+                        symbols_txt))
 
         # Fortran requires use statements to be specified before
         # variable declarations. As a convention, this method also
@@ -746,7 +768,7 @@ class FortranWriter(PSyIRVisitor):
         # variables at the routine level scope. For this reason, at this
         # point we have to unify all declarations and resolve possible name
         # clashes that appear when merging the scopes.
-        whole_routine_scope = SymbolTable()
+        whole_routine_scope = SymbolTable(node)
         for schedule in node.walk(Schedule):
             for symbol in schedule.symbol_table.symbols:
                 try:
diff --git a/src/psyclone/psyir/symbols/symboltable.py b/src/psyclone/psyir/symbols/symboltable.py
index a2fe85132..97f9d647b 100644
--- a/src/psyclone/psyir/symbols/symboltable.py
+++ b/src/psyclone/psyir/symbols/symboltable.py
@@ -1011,6 +1011,23 @@ class SymbolTable(object):
         # Re-insert modified symbol
         self.add(symbol)
 
+    def has_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
+
+        '''
+        current_table = self
+        while current_table:
+            for sym in current_table.containersymbols:
+                if sym.wildcard_import:
+                    return True
+            current_table = current_table.parent_symbol_table()
+        return False
+
     def view(self):
         '''
         Print a representation of this Symbol Table to stdout.


Test patch:
diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py
index 88bd1bae2..694350b6a 100644
--- a/src/psyclone/tests/psyir/backend/fortran_test.py
+++ b/src/psyclone/tests/psyir/backend/fortran_test.py
@@ -49,7 +49,7 @@ from psyclone.psyir.backend.fortran import gen_intent, FortranWriter, \
 from psyclone.psyir.nodes import Node, CodeBlock, Container, Literal, \
     UnaryOperation, BinaryOperation, NaryOperation, Reference, Call, \
     KernelSchedule, ArrayReference, ArrayOfStructuresReference, Range, \
-    StructureReference, Schedule
+    StructureReference, Schedule, Routine, Return
 from psyclone.psyir.symbols import DataSymbol, SymbolTable, ContainerSymbol, \
     GlobalInterface, ArgumentInterface, UnresolvedInterface, ScalarType, \
     ArrayType, INTEGER_TYPE, REAL_TYPE, CHARACTER_TYPE, BOOLEAN_TYPE, \
@@ -684,22 +684,82 @@ def test_gen_decls(fort_writer):
     with pytest.raises(VisitorError) as excinfo:
         _ = fort_writer.gen_decls(symbol_table)
     assert ("The following symbols are not explicitly declared or imported "
-            "from a module (in the local scope) and are not KIND parameters: "
+            "from a module and there are no wildcard "
+            "imports which could be bringing them into scope: "
             "'unknown'" in str(excinfo.value))
 
 
+def test_gen_decls_nested_scope(fort_writer):
+    ''' Test that gen_decls() correctly checks for potential wildcard imports
+    of an unresolved symbol in an outer scope.
+
+    '''
+    inner_table = SymbolTable()
+    inner_table.add(DataSymbol("unknown1", INTEGER_TYPE,
+                               interface=UnresolvedInterface()))
+    routine = Routine.create("my_func", inner_table, [Return()])
+    cont_table = SymbolTable()
+    _ = Container.create("my_mod", cont_table, [routine])
+
+    cont_table.add(ContainerSymbol("my_module"))
+    # Innermost symbol table contains "unknown1" and there's no way it can
+    # be brought into scope
+    with pytest.raises(VisitorError) as err:
+        fort_writer.gen_decls(inner_table)
+    assert ("symbols are not explicitly declared or imported from a module "
+            "and there are no wildcard imports which "
+            "could be bringing them into scope: 'unknown1'" in str(err.value))
+    # Add a ContainerSymbol with a wildcard import in the outermost scope
+    csym = ContainerSymbol("other_mod")
+    csym.wildcard_import = True
+    cont_table.add(csym)
+    # The inner symbol table contains a symbol with an unresolved interface
+    # but nothing that requires an actual declaration
+    result = fort_writer.gen_decls(inner_table)
+    assert result == ""
+    # Move the wildcard import into the innermost table
+    cont_table.remove(csym)
+    inner_table.add(csym)
+    result = fort_writer.gen_decls(inner_table)
+    assert result == ""
+
+
 def test_gen_decls_routine(fort_writer):
     '''Test that the gen_decls method raises an exception if the interface
-    of a routine symbol is not a GlobalInterface.
+    of a routine symbol is not a GlobalInterface, unless there's a wildcard
+    import from a Container.
 
     '''
     symbol_table = SymbolTable()
-    symbol_table.add(RoutineSymbol("arg_sub", interface=ArgumentInterface()))
+    # Check that a RoutineSymbol representing an intrinsic is OK
+    symbol_table.add(RoutineSymbol("nint", interface=UnresolvedInterface()))
+    result = fort_writer.gen_decls(symbol_table)
+    assert result == ""
+    # Now add a user-defined routine symbol but with an (unsupported)
+    # ArgumentInterface
+    rsym = RoutineSymbol("arg_sub", interface=ArgumentInterface())
+    symbol_table.add(rsym)
+    with pytest.raises(VisitorError) as info:
+        _ = fort_writer.gen_decls(symbol_table)
+    assert ("Routine symbol 'arg_sub' is passed as an argument (has an "
+            "ArgumentInterface). This is not supported by the Fortran "
+            "back-end." in str(info.value))
+    # Replace that symbol with one that has a deferred interface
+    symbol_table.remove(rsym)
+    symbol_table.add(RoutineSymbol("sub2", interface=UnresolvedInterface()))
     with pytest.raises(VisitorError) as info:
         _ = fort_writer.gen_decls(symbol_table)
     assert (
-        "Routine symbols without a global or local interface are not supported"
-        " by the Fortran back-end." in str(info.value))
+        "Routine symbol 'sub2' does not have a GlobalInterface or "
+        "LocalInterface, is not a Fortran intrinsic and there is no wildcard "
+        "import which could bring it into scope. This is not supported by the "
+        "Fortran back-end." in str(info.value))
+    # Now add a wildcard import from a ContainerSymbol
+    csym = ContainerSymbol("some_mod")
+    csym.wildcard_import = True
+    symbol_table.add(csym)
+    result = fort_writer.gen_decls(symbol_table)
+    assert result == ""
 
 
 def test_gen_routine_access_stmts(fort_writer):
diff --git a/src/psyclone/tests/psyir/symbols/symboltable_test.py b/src/psyclone/tests/psyir/symbols/symboltable_test.py
index 4fde3e0ff..ac7865888 100644
--- a/src/psyclone/tests/psyir/symbols/symboltable_test.py
+++ b/src/psyclone/tests/psyir/symbols/symboltable_test.py
@@ -759,6 +759,22 @@ def test_lookup_with_tag_3():
                     in str(info.value))
 
 
+def test_has_wildcard_imports():
+    ''' Test the has_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
+    csym = 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
+    # 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
+
+
 def test_view(capsys):
     '''Test the view method of the SymbolTable class, it should print to
     standard out a representation of the full SymbolTable.'''