Repository: stfc/PSyclone

Issue:
LFRic alg layer error when using PSyIR: unknown kernel symbol
The version of `mixed_schur_preconditioner_alg_mod` on LFRic trunk is causing LFRic PSyIR alg-layer parsing to fail as `dg_inc_matrix_vector_kernel_type` is not imported in subroutine `back_substitute`. This is an error in the LFRic code which I've passed on but I need to tidy the error produced by PSyclone to make it clearer what the problem is.

Discussion:

Patch:
diff --git a/changelog b/changelog
index dab8c8548..060343dfb 100644
--- a/changelog
+++ b/changelog
@@ -527,6 +527,9 @@
 	new, associated optimisation scripts. Includes bug fixes for
 	matmul inlining transformation.
 
+	179) PR #2230 for #2228. Improve reporting of errors due to kernel
+	functors not explicitly included in algorithm use statements.
+
 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/generator.py b/src/psyclone/generator.py
index c3d2aafc3..69dc40bdf 100644
--- a/src/psyclone/generator.py
+++ b/src/psyclone/generator.py
@@ -61,6 +61,7 @@ from psyclone.domain.common.transformations import AlgTrans
 from psyclone.domain.gocean.transformations import (
     RaisePSyIR2GOceanKernTrans, GOceanAlgInvoke2PSyCallTrans)
 from psyclone.domain.lfric.algorithm import LFRicBuiltinFunctor
+from psyclone.domain.lfric.lfric_builtins import BUILTIN_MAP
 from psyclone.domain.lfric.transformations import (
     LFRicAlgTrans, RaisePSyIR2LFRicKernTrans, LFRicAlgInvoke2PSyCallTrans)
 from psyclone.errors import GenerationError, InternalError
@@ -75,6 +76,7 @@ from psyclone.psyir.backend.fortran import FortranWriter
 from psyclone.psyir.frontend.fortran import FortranReader
 from psyclone.psyir.frontend.fparser2 import Fparser2Reader
 from psyclone.psyir.nodes import Loop, Container, Routine
+from psyclone.psyir.symbols import UnresolvedInterface
 from psyclone.psyir.transformations import TransformationError
 from psyclone.version import __VERSION__
 
@@ -92,7 +94,6 @@ LFRIC_TESTING = False
 
 
 def handle_script(script_name, info, function_name, is_optional=False):
-    # pylint: disable=too-many-locals
     '''Loads and applies the specified script to the given algorithm or
     psy layer. The relevant script function (in 'function_name') is
     called with 'info' as the argument.
@@ -188,34 +189,39 @@ def generate(filename, api="", kernel_paths=None, script_name=None,
 
     :param str filename: the file containing the algorithm specification.
     :param str api: the name of the API to use. Defaults to empty string.
-    :param kernel_paths: the directories from which to recursively \
-        search for the files containing the kernel source (if \
-        different from the location of the algorithm specification). \
+    :param kernel_paths: the directories from which to recursively
+        search for the files containing the kernel source (if
+        different from the location of the algorithm specification).
         Defaults to None.
     :type kernel_paths: Optional[List[str]]
-    :param str script_name: a script file that can apply optimisations \
-        to the PSy layer (can be a path to a file or a filename that \
+    :param str script_name: a script file that can apply optimisations
+        to the PSy layer (can be a path to a file or a filename that
         relies on the PYTHONPATH to find the module). Defaults to None.
-    :param bool line_length: a logical flag specifying whether we care \
-        about line lengths being longer than 132 characters. If so, \
-        the input (algorithm and kernel) code is checked to make sure \
+    :param bool line_length: a logical flag specifying whether we care
+        about line lengths being longer than 132 characters. If so,
+        the input (algorithm and kernel) code is checked to make sure
         that it conforms. The default is False.
-    :param bool distributed_memory: a logical flag specifying whether \
-        to generate distributed memory code. The default is set in the \
+    :param bool distributed_memory: a logical flag specifying whether
+        to generate distributed memory code. The default is set in the
         'config.py' file.
-    :param str kern_out_path: directory to which to write transformed \
+    :param str kern_out_path: directory to which to write transformed
         kernel code. Defaults to empty string.
-    :param bool kern_naming: the scheme to use when re-naming transformed \
+    :param bool kern_naming: the scheme to use when re-naming transformed
         kernels. Defaults to "multiple".
-    :return: 2-tuple containing the fparser1 AST for the algorithm code and \
+    :return: 2-tuple containing the fparser1 AST for the algorithm code and
         the fparser1 AST or a string (for NEMO) of the psy code.
-    :rtype: Tuple[:py:class:`fparser.one.block_statements.BeginSource`, \
-            :py:class:`fparser.one.block_statements.Module`] | \
-            Tuple[:py:class:`fparser.one.block_statements.BeginSource`, str]
+    :rtype: Tuple[:py:class:`fparser.one.block_statements.BeginSource`,
+        :py:class:`fparser.one.block_statements.Module`] |
+        Tuple[:py:class:`fparser.one.block_statements.BeginSource`, str]
 
     :raises GenerationError: if an invalid API is specified.
     :raises GenerationError: if an invalid kernel-renaming scheme is specified.
+    :raises GenerationError: if there is an error raising the PSyIR to
+        domain-specific PSyIR.
+    :raises GenerationError: if a kernel functor is not named in a use
+        statement.
     :raises IOError: if the filename or search path do not exist.
+    :raises NoInvokesError: if no invokes are found in the algorithm file.
 
     For example:
 
@@ -318,6 +324,27 @@ def generate(filename, api="", kernel_paths=None, script_name=None,
                 if isinstance(kern, LFRicBuiltinFunctor):
                     # Skip builtins
                     continue
+                if isinstance(kern.symbol.interface, UnresolvedInterface):
+                    # This kernel functor is not specified in a use statement.
+                    # Find all container symbols that are in scope.
+                    st_ref = kern.scope.symbol_table
+                    container_symbols = [
+                        symbol.name for symbol in st_ref.containersymbols]
+                    while st_ref.parent_symbol_table():
+                        st_ref = st_ref.parent_symbol_table()
+                        container_symbols += [
+                            symbol.name for symbol in st_ref.containersymbols]
+                    message = (
+                        f"Kernel functor '{kern.symbol.name}' in routine "
+                        f"'{kern.scope.name}' from algorithm file "
+                        f"'{filename}' must be named in a use "
+                        f"statement (found {container_symbols})")
+                    if api == "dynamo0.3":
+                        message += (
+                            f" or be a recognised built-in (one of "
+                            f"{list(BUILTIN_MAP.keys())})")
+                    message += "."
+                    raise GenerationError(message)
                 container_symbol = kern.symbol.interface.container_symbol
 
                 # Find the kernel file containing the container


Test patch:
diff --git a/src/psyclone/tests/generator_test.py b/src/psyclone/tests/generator_test.py
index d269c3db6..b21925a3b 100644
--- a/src/psyclone/tests/generator_test.py
+++ b/src/psyclone/tests/generator_test.py
@@ -45,8 +45,10 @@ functions.
 
 import os
 import re
+import shutil
 import stat
 from sys import modules
+
 import pytest
 
 from fparser.common.readfortran import FortranStringReader
@@ -1274,3 +1276,92 @@ def test_no_invokes_lfric_new(monkeypatch):
             api="dynamo0.3")
     assert ("Algorithm file contains no invoke() calls: refusing to generate "
             "empty PSy code" in str(info.value))
+
+
+def test_generate_unknown_container_lfric(tmpdir, monkeypatch):
+    '''Test that a GenerationError exception in the generate function is
+    raised for the LFRic DSL if one of the functors is not explicitly
+    declared. This can happen in LFRic algorithm code as it is never
+    compiled. The exception is only raised with the new PSyIR approach
+    to modify the algorithm layer which is currently in development so
+    is protected by a switch. This switch is turned on in this test by
+    monkeypatching.
+
+    At the moment this exception is only raised if the functor is
+    declared in a different subroutine or function, as the original
+    parsing approach picks up all other cases. However, the original
+    parsing approach will eventually be removed.
+
+    '''
+    monkeypatch.setattr(generator, "LFRIC_TESTING", True)
+    code = (
+        "module some_kernel_mod\n"
+        "use module_mod, only : module_type\n"
+        "contains\n"
+        "subroutine dummy_kernel()\n"
+        " use testkern_mod, only: testkern_type\n"
+        "end subroutine dummy_kernel\n"
+        "subroutine some_kernel()\n"
+        "  use constants_mod, only: r_def\n"
+        "  use field_mod, only : field_type\n"
+        "  type(field_type) :: field1, field2, field3, field4\n"
+        "  real(kind=r_def) :: scalar\n"
+        "  call invoke(testkern_type(scalar, field1, field2, field3, "
+        "field4))\n"
+        "end subroutine some_kernel\n"
+        "end module some_kernel_mod\n")
+    alg_filename = str(tmpdir.join("alg.f90"))
+    with open(alg_filename, "w", encoding='utf-8') as my_file:
+        my_file.write(code)
+    kern_filename = os.path.join(DYN03_BASE_PATH, "testkern_mod.F90")
+    shutil.copyfile(kern_filename, str(tmpdir.join("testkern_mod.F90")))
+    with pytest.raises(GenerationError) as info:
+        _, _ = generate(alg_filename)
+    assert ("Kernel functor 'testkern_type' in routine 'some_kernel' from "
+            "algorithm file '" in str(info.value))
+    assert ("alg.f90' must be named in a use statement (found ["
+            "'constants_mod', 'field_mod', '_psyclone_builtins', "
+            "'module_mod']) or be a recognised built-in (one of "
+            "['x_plus_y', 'inc_x_plus_y'," in str(info.value))
+
+
+def test_generate_unknown_container_gocean(tmpdir):
+    '''Test that a GenerationError exception in the generate function is
+    raised for the GOcean DSL if one of the functors is not explicitly
+    declared. This can happen in GOcean algorithm code as it is never
+    compiled.
+
+    At the moment this exception is only raised if the functor is
+    declared in a different subroutine or function, as the original
+    parsing approach picks up all other cases. However, the original
+    parsing approach will eventually be removed.
+
+    '''
+    code = (
+        "module some_kernel_mod\n"
+        "use module_mod, only : module_type\n"
+        "contains\n"
+        "subroutine dummy_kernel()\n"
+        "  use compute_cu_mod,  only: compute_cu\n"
+        "end subroutine dummy_kernel\n"
+        "subroutine some_kernel()\n"
+        "  use kind_params_mod\n"
+        "  use grid_mod, only: grid_type\n"
+        "  use field_mod, only: r2d_field\n"
+        "  type(grid_type), target :: model_grid\n"
+        "  type(r2d_field) :: p_fld, u_fld, cu_fld\n"
+        "  call invoke( compute_cu(cu_fld, p_fld, u_fld) )\n"
+        "end subroutine some_kernel\n"
+        "end module some_kernel_mod\n")
+    alg_filename = str(tmpdir.join("alg.f90"))
+    with open(alg_filename, "w", encoding='utf-8') as my_file:
+        my_file.write(code)
+    kern_filename = os.path.join(GOCEAN_BASE_PATH, "compute_cu_mod.f90")
+    shutil.copyfile(kern_filename, str(tmpdir.join("compute_cu_mod.f90")))
+    with pytest.raises(GenerationError) as info:
+        _, _ = generate(alg_filename, api="gocean1.0")
+    assert ("Kernel functor 'compute_cu' in routine 'some_kernel' from "
+            "algorithm file '" in str(info.value))
+    assert ("alg.f90' must be named in a use statement (found "
+            "['kind_params_mod', 'grid_mod', 'field_mod', 'module_mod'])."
+            in str(info.value))