Repository: stfc/PSyclone

Issue:
PSyAd to not assume kernel name from module name
Currently PSyAd assumes that the name of the kernel metadata type will be the name of the module with the `_mod` replaced with `_type`. This will not always be the case.

The function `psyclone.domain.lfric.utils.metadata_name_from_module_name()` does this.

It probably needs to be replaced with a `get_metadata_name_from_module()` which parses the module source and identifies types extending `kernel_type`.

Discussion:

Patch:
diff --git a/changelog b/changelog
index d790537af..1c1d9881c 100644
--- a/changelog
+++ b/changelog
@@ -184,6 +184,11 @@
 	63) PR #2660 for #1370. Adds 'collapse' and 'ignore_dependencies_for'
 	options to ParallelLoopTrans.
 
+	64) PR #2653. Add example using the PSyclone xDSL MLIR backend.
+
+	65) PR #2673 for #2652. Find the symbol name containing a kernel metadata
+	by searching the PSyIR (instead of assuming the name).
+
 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/domain/lfric/utils.py b/src/psyclone/domain/lfric/utils.py
index 53d2853fb..69d45269f 100644
--- a/src/psyclone/domain/lfric/utils.py
+++ b/src/psyclone/domain/lfric/utils.py
@@ -100,37 +100,3 @@ def find_container(psyir):
     raise GenerationError(
         "The supplied PSyIR contains more than two Containers. This is not "
         "a valid LFRic kernel.")
-
-
-def metadata_name_from_module_name(module_name):
-    '''Utility that returns the expected kernel metadata name given the
-    kernel module name for an LFRic kernel. It is possible to infer
-    this as LFRic requires the names to be written in a particular
-    form.
-
-    :param str module_name: the name of the kernel module.
-
-    :returns: the expected name of the kernel metadata.
-    :rtype: str
-
-    :except TypeError: if the module_name argument is not the expected
-        type.
-    :except GenerationError: if the module name does not conform to
-        the LFRic kernel naming conventions.
-
-    '''
-    if not isinstance(module_name, str):
-        raise TypeError(
-            f"Expected the module_name argument to the "
-            f"metadata_name_from_module_name utility to be a string, but "
-            f"found '{type(module_name).__name__}'.")
-
-    len_mod = len("_mod")
-    if module_name.lower().endswith("_mod") and len(module_name) > len_mod:
-        root_name = module_name[:-len_mod]
-    else:
-        raise GenerationError(
-            f"LFRic module names should end with \"_mod\", with at least "
-            f"one preceding character, but found a module called "
-            f"'{module_name}'.")
-    return f"{root_name}_type"
diff --git a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py
index 6115b87c8..ae5693fa2 100644
--- a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py
+++ b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py
@@ -47,12 +47,12 @@ from psyclone.domain.lfric.kernel import (
     ColumnwiseOperatorArgMetadata, FieldVectorArgMetadata)
 from psyclone.domain.lfric.transformations import RaisePSyIR2LFRicKernTrans
 from psyclone.domain.lfric.utils import (
-    find_container, metadata_name_from_module_name)
+    find_container)
 from psyclone.errors import InternalError, GenerationError
 from psyclone.psyad import AdjointVisitor
 from psyclone.psyad.domain.common import create_adjoint_name
 from psyclone.psyir.nodes import Routine
-from psyclone.psyir.symbols import ContainerSymbol
+from psyclone.psyir.symbols import ContainerSymbol, UnsupportedFortranType
 from psyclone.psyir.symbols.symbol import ArgumentInterface, ImportInterface
 
 
@@ -61,25 +61,32 @@ def generate_lfric_adjoint(tl_psyir, active_variables):
     '''Takes an LFRic tangent-linear kernel represented in language-level PSyIR
     and returns its adjoint represented in language-level PSyIR.
 
-    :param tl_psyir: language-level PSyIR containing the LFRic \
+    :param tl_psyir: language-level PSyIR containing the LFRic
         tangent-linear kernel.
     :type tl_psyir: :py:class:`psyclone.psyir.Node`
-    :param list of str active_variables: list of active variable names.
+    :param list[str] active_variables: names of the active variables.
 
-    :returns: language-level PSyIR containing the adjoint of the \
+    :returns: language-level PSyIR containing the adjoint of the
         supplied tangent-linear kernel.
     :rtype: :py:class:`psyclone.psyir.Node`
 
-    :raises InternalError: if the PSyIR does not have a Container.
+    :raises InternalError: if the PSyIR does not contain any kernel metadata.
     :raises InternalError: if the PSyIR does not contain any Routines.
 
     '''
     logger = logging.getLogger(__name__)
 
-    # Infer the tangent-linear metadata name from the container
-    # name. This will be used later to find and modify the metadata.
+    # Find the name of the symbol containing the metadata for the tangent-
+    # linear kernel.
     tl_container = find_container(tl_psyir)
-    tl_metadata_name = metadata_name_from_module_name(tl_container.name)
+    for sym in tl_container.symbol_table.datatypesymbols:
+        if (isinstance(sym.datatype, UnsupportedFortranType) and
+                "extends(kernel_type)" in sym.datatype.declaration.lower()):
+            tl_metadata_name = sym.name
+            break
+    else:
+        raise InternalError("The supplied PSyIR does not contain any kernel "
+                            "metadata")
 
     # Translate from TL to AD
     logger.debug("Translating from TL to AD.")


Test patch:
diff --git a/src/psyclone/tests/domain/lfric/test_utils.py b/src/psyclone/tests/domain/lfric/test_utils.py
index edea4a726..1f9dd338c 100644
--- a/src/psyclone/tests/domain/lfric/test_utils.py
+++ b/src/psyclone/tests/domain/lfric/test_utils.py
@@ -37,8 +37,7 @@
 
 import pytest
 
-from psyclone.domain.lfric.utils import (
-    find_container, metadata_name_from_module_name)
+from psyclone.domain.lfric.utils import find_container
 from psyclone.errors import GenerationError, InternalError
 from psyclone.psyir.nodes import Routine, FileContainer, Container
 from psyclone.psyir.symbols import SymbolTable
@@ -140,43 +139,3 @@ def test_find_container_working():
     assert result is module
     result = find_container(module)
     assert result is module
-
-
-# metadata_name_from_module_name
-
-def test_metadata_name_wrong_type():
-    '''Test that the expected exception is raised if the supplied module
-    name is the wrong type.
-
-    '''
-    with pytest.raises(TypeError) as info:
-        _ = metadata_name_from_module_name(None)
-    assert ("Expected the module_name argument to the "
-            "metadata_name_from_module_name utility to be a string, but "
-            "found 'NoneType'." in str(info.value))
-
-
-@pytest.mark.parametrize("name", ["", "_mod", "module"])
-def test_metadata_name_no_mod(name):
-    '''Test that the expected exception is raised if the supplied module
-    name does not end in _mod or does not have at least one character
-    that precedes _mod.
-
-    '''
-    with pytest.raises(GenerationError) as info:
-        _ = metadata_name_from_module_name(name)
-    assert (f"LFRic module names should end with \"_mod\", with at least "
-            f"one preceding character, but found a module called '{name}'."
-            in str(info.value))
-
-
-@pytest.mark.parametrize("name,expected", [
-    ("example_mod", "example_type"), ("x_mod", "x_type"),
-    ("something_mod_mod", "something_mod_type")])
-def test_metadata_name_ok(name, expected):
-    '''Test that the funtion returns the expected output if valid input is
-    provided (*_mod in the input is output as *_type).
-
-    '''
-    result = metadata_name_from_module_name(name)
-    assert result == expected
diff --git a/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py
index 0a98864cc..e2b713de0 100644
--- a/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py
+++ b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py
@@ -52,7 +52,6 @@ from psyclone.psyad.domain.lfric.lfric_adjoint import (
     _update_access_metadata, _check_or_add_access_symbol)
 from psyclone.psyir.symbols import (
     DataSymbol, ArgumentInterface, INTEGER_TYPE, REAL_TYPE)
-from psyclone.psyir.transformations import TransformationError
 
 
 def test_generate_lfric_adjoint_no_container_error(fortran_reader):
@@ -75,10 +74,10 @@ end program test
             "source is not within a module)." in str(err.value))
 
 
-def test_generate_lfric_adjoint_no_routines_error(fortran_reader):
+def test_generate_lfric_adjoint_no_metadata_error(fortran_reader):
     '''
     Check that generate_lfric_adjoint raises the expected error when
-    provided with PSyIR that does not contain any Routines.
+    provided with PSyIR that does not contain any kernel metadata.
 
     '''
     psyir = fortran_reader.psyir_from_source("""\
@@ -89,25 +88,49 @@ end module test_mod
 """)
     with pytest.raises(InternalError) as err:
         generate_lfric_adjoint(psyir, ["var1", "var2"])
-    assert "The supplied PSyIR does not contain any routines" in str(err.value)
+    assert ("The supplied PSyIR does not contain any kernel metadata"
+            in str(err.value))
 
 
-MULTI_ROUTINE_CODE = (
-    "module test_mod\n"
-    "  implicit none\n"
-    "  use kernel_mod\n"
-    "  use argument_mod\n"
+METADATA = (
     "  type, extends(kernel_type) :: test_type\n"
     "     type(arg_type), dimension(2) :: meta_args = (/  &\n"
     "          arg_type(gh_field,  gh_real, gh_inc, w0),  &\n"
     "          arg_type(gh_field,  gh_real, gh_read, w0)  &\n"
     "          /)\n"
     "     integer :: operates_on = cell_column\n"
-    "  end type test_type\n"
-    "  public test_code\n"
-    "  interface test_code\n"
-    "    module procedure :: test_code_r4, test_code_r8\n"
-    "  end interface test_code\n"
+    "  end type test_type\n")
+
+
+def test_generate_lfric_adjoint_no_routines_error(fortran_reader):
+    '''
+    Check that generate_lfric_adjoint raises the expected error when
+    provided with PSyIR that does not contain any Routines.
+
+    '''
+    psyir = fortran_reader.psyir_from_source(f"""\
+module test_mod
+  implicit none
+  integer :: var1
+  {METADATA}
+end module test_mod
+""")
+    with pytest.raises(InternalError) as err:
+        generate_lfric_adjoint(psyir, ["var1", "var2"])
+    assert ("The supplied PSyIR does not contain any routines"
+            in str(err.value))
+
+
+MULTI_ROUTINE_CODE = (
+    f"module test_mod\n"
+    f"  implicit none\n"
+    f"  use kernel_mod\n"
+    f"  use argument_mod\n"
+    f"{METADATA}"
+    f"  public test_code\n"
+    f"  interface test_code\n"
+    f"    module procedure :: test_code_r4, test_code_r8\n"
+    f"  end interface test_code\n"
     # Include a second interface that also references the two subroutines.
     "  interface another_interface\n"
     "    module procedure :: test_code_r8, test_code_r4\n"
@@ -186,19 +209,15 @@ SINGLE_ROUTINE_CODE = (
     "end module test_mod")
 
 
-def test_generate_lfric_adjoint_no_metadata(fortran_reader):
+def test_generate_lfric_adjoint_no_name_convention(fortran_reader):
     '''Check that the expected error is raised when the metadata has an
-    unexpected name (i.e. does not conform to the LFRic coding
-    standards).
+    unexpected name.
 
     '''
     psyir = fortran_reader.psyir_from_source(
         SINGLE_ROUTINE_CODE.replace("test_type", "wrong_name"))
-    with pytest.raises(TransformationError) as err:
-        generate_lfric_adjoint(psyir, ["var1", "var2"])
-    assert ("The metadata name 'test_type' provided to the transformation "
-            "does not correspond to a symbol in the supplied PSyIR."
-            in str(err.value))
+    adj_psyir = generate_lfric_adjoint(psyir, ["var1", "var2"])
+    assert adj_psyir
 
 
 def test_generate_lfric_adjoint_type_and_procedure_names(