From 2c69f8687e69c121d08f986d91498e26990b083c Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Tue, 18 Aug 2026 13:37:53 +0100 Subject: [PATCH 01/12] Fix issue with LFRic GPU integration tests --- .../domain/common/transformations/kernel_module_inline_trans.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index 75b36d4549..b409f34685 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -440,6 +440,8 @@ def apply(self, # If we haven't copied in a routine of 'caller_name' then it must # be because the target of the call is renamed on import. target_sym = name_map.get(external_callee_name) + if not target_sym: + raise TransformationError("Could not find target symbol") for call in all_calls: name = call.routine.symbol.name.lower() From 081226a36117671cc4b6a95f73339f08157762ba Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Wed, 19 Aug 2026 16:09:37 +0100 Subject: [PATCH 02/12] Fix ModuleInline when the capitalisation of the subroutines are different --- .../transformations/kernel_module_inline_trans.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index b409f34685..ceb8794e89 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -411,7 +411,7 @@ def apply(self, # collisions. new_sym.visibility = Symbol.Visibility.PRIVATE # Add the new symbol to the map. - name_map[code_to_inline.name] = new_sym + name_map[code_to_inline.name.lower()] = new_sym # Add the routine code into this Container code_to_inline = code_to_inline.detach() code_to_inline.symbol = new_sym @@ -425,7 +425,7 @@ def apply(self, symbol_type=GenericInterfaceSymbol, routines=[(sym, True) for sym in name_map.values()], visibility=Symbol.Visibility.PRIVATE) - name_map[interface_sym.name] = new_sym + name_map[interface_sym.name.lower()] = new_sym if update_all: # We will update all Calls/Kernels associated with the @@ -435,13 +435,11 @@ def apply(self, # Only update the supplied Call/Kernel. all_calls = [node] - target_sym = name_map.get(caller_name, None) + target_sym = name_map.get(caller_name.lower(), None) if not target_sym: # If we haven't copied in a routine of 'caller_name' then it must # be because the target of the call is renamed on import. - target_sym = name_map.get(external_callee_name) - if not target_sym: - raise TransformationError("Could not find target symbol") + target_sym = name_map.get(external_callee_name.lower()) for call in all_calls: name = call.routine.symbol.name.lower() From dfeea49b4258b427293978a590212654bb5d013c Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Tue, 18 Aug 2026 12:11:02 +0100 Subject: [PATCH 03/12] Add support for parsing derived type extends and contains blocks --- src/psyclone/psyir/frontend/fparser2.py | 84 ++++++++- src/psyclone/psyir/symbols/datatypes.py | 172 +++++++++++++++++- .../frontend/fparser2_derived_type_test.py | 92 ++++++++-- .../tests/psyir/symbols/datatype_test.py | 143 ++++++++++++++- 4 files changed, 468 insertions(+), 23 deletions(-) diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 2a6b338999..2620a2379c 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -2281,14 +2281,32 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): # Populate this StructureType by processing the components of # the derived type try: - # We don't support derived-types with additional - # attributes e.g. "extends" or "abstract". Note, we do - # support public/private attributes but these are stored - # as Access_Spec, not Type_Attr_Spec. + # EXTENDS is the only additional derived-type attribute that we + # currently support. Note that public/private attributes are + # represented by Access_Spec rather than Type_Attr_Spec. derived_type_stmt = decl.children[0] - if walk(derived_type_stmt, Fortran2003.Type_Attr_Spec): - raise NotImplementedError( - "Derived-type definition contains unsupported attributes.") + for attr in walk(derived_type_stmt, + Fortran2003.Type_Attr_Spec): + if attr.items[0].upper() != "EXTENDS": + raise NotImplementedError( + "Derived-type definition contains unsupported " + "attributes.") + + extends_name = attr.items[1].string + extends_symbol = parent.symbol_table.lookup( + extends_name, otherwise=None) + if extends_symbol is None: + extends_symbol = DataTypeSymbol( + extends_name, StructureType(), + interface=UnresolvedInterface()) + parent.symbol_table.add(extends_symbol) + elif type(extends_symbol) is Symbol: + # The name may already have been introduced by a USE + # statement for which no declaration information was + # available. + extends_symbol.specialise(DataTypeSymbol) + extends_symbol.datatype = StructureType() + dtype.extends = extends_symbol # Re-use the existing code for processing symbols. This needs to # be able to find any symbols declared in an outer scope but @@ -2309,6 +2327,10 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): parent, local_table, component, preceding_comments=preceding_comments) preceding_comments = [] + elif isinstance( + child, Fortran2003.Type_Bound_Procedure_Part): + self._process_derived_type_contains_block( + parent, child, dtype) elif isinstance(child, (Fortran2003.Private_Components_Stmt, Fortran2003.End_Type_Stmt)): continue @@ -2344,6 +2366,54 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): return tsymbol + @staticmethod + def _process_derived_type_contains_block( + parent: ScopingNode, + contains: Fortran2003.Type_Bound_Procedure_Part, + dtype: StructureType + ): + '''Process type-bound procedures in a derived type's CONTAINS part. + + Currently all bindings are UnsupportedFortranType, but its name and + visibility is parsed in order to add the correct component in the + parent's StructureType. + + :param parent: PSyIR scope containing the derived-type declaration. + :param contains: fparser2 type-bound-procedure part. + :param dtype: StructureType being populated. + ''' + private_stmts = walk(contains, + Fortran2003.Binding_Private_Stmt) + default_visibility = (Symbol.Visibility.PRIVATE if private_stmts + else Symbol.Visibility.PUBLIC) + + for procedure in walk(contains, Fortran2003.Specific_Binding): + binding_name = procedure.items[3].string + visibility = default_visibility + if procedure.items[1] is not None: + access_specs = walk(procedure.items[1], + Fortran2003.Access_Spec) + if access_specs: + visibility = _process_access_spec(access_specs[0]) + + target = None + if procedure.items[4] is not None: + target_name = procedure.items[4].string + target_symbol = parent.symbol_table.lookup( + target_name, otherwise=None) + if target_symbol is None: + target_symbol = RoutineSymbol( + target_name, interface=UnresolvedInterface()) + parent.symbol_table.add(target_symbol) + elif type(target_symbol) is Symbol: + target_symbol.specialise(RoutineSymbol) + target_symbol.datatype = UnresolvedType() + target = Reference(target_symbol) + + dtype.add_procedure_component( + binding_name, UnsupportedFortranType(str(procedure)), + visibility, target) + def _get_partial_datatype( self, node: Fortran2003.Type_Declaration_Stmt, diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 99b5d7658c..6f1b9f91b9 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -1253,7 +1253,8 @@ class ComponentType(CommentableMixin): :param name: the name of the member. :param datatype: the type of the member. :param visibility: whether this member is public or private. - :param initial_value: the initial value of this member (if any). + :param initial_value: the initial value of this member (if any) or the + redirection value if it is a procedure. :type initial_value: Optional[:py:class:`psyclone.psyir.nodes.Node`] ''' name: str @@ -1263,6 +1264,8 @@ class ComponentType(CommentableMixin): def __init__(self): self._components = OrderedDict() + self._procedure_components = OrderedDict() + self._extends = None def __str__(self): return "StructureType<>" @@ -1278,10 +1281,16 @@ def __copy__(self): new.add(name, component.datatype, component.visibility, component.initial_value, component.preceding_comment, component.inline_comment) + for name, component in self.procedure_components.items(): + new.add_procedure_component( + name, component.datatype, component.visibility, + component.initial_value, component.preceding_comment, + component.inline_comment) + new._extends = self.extends return new @staticmethod - def create(components): + def create(components, procedure_components=None, extends=None): ''' Creates a StructureType from the supplied list of properties. @@ -1297,6 +1306,12 @@ def create(components): Optional[str], Optional[str] ]] + :param procedure_components: the procedure bindings of this type, + specified in the same way as data components. + :type procedure_components: Optional[List[tuple]] + :param extends: the type extended by this type, if any. + :type extends: Optional[ + :py:class:`psyclone.psyir.symbols.DataTypeSymbol`] :returns: the new type object. :rtype: :py:class:`psyclone.psyir.symbols.StructureType` @@ -1311,6 +1326,18 @@ def create(components): f"preceding_comment, inline_comment) but found a " f"tuple with {len(component)} members: {component}") stype.add(*component) + if procedure_components: + for component in procedure_components: + if len(component) not in (3, 4, 5, 6): + raise TypeError( + f"Each procedure component must be specified using a " + f"3 to 6-tuple of (name, type, visibility, " + f"initial_value, preceding_comment, inline_comment) " + f"but found a tuple with {len(component)} members: " + f"{component}") + stype.add_procedure_component(*component) + if extends is not None: + stype.extends = extends return stype @property @@ -1321,6 +1348,38 @@ def components(self): ''' return self._components + @property + def procedure_components(self): + ''' + :returns: ordered dictionary of the type-bound procedures of this + type. + :rtype: :py:class:`collections.OrderedDict` + ''' + return self._procedure_components + + @property + def extends(self): + ''' + :returns: the type extended by this type, or None. + :rtype: Optional[ + :py:class:`psyclone.psyir.symbols.DataTypeSymbol`] + ''' + return self._extends + + @extends.setter + def extends(self, value: Symbol): + '''Set the type extended by this type. + + :param value: the type being extended. + + :raises TypeError: if value is not a Symbol. + ''' + if not isinstance(value, Symbol): + raise TypeError( + f"The type that a StructureType extends must be a " + f"Symbol but got '{type(value).__name__}'.") + self._extends = value + def add(self, name: str, datatype, visibility, initial_value=None, preceding_comment: str = "", inline_comment: str = ""): ''' @@ -1402,7 +1461,79 @@ def lookup(self, name): StructureType. :rtype: :py:class:`psyclone.psyir.symbols.StructureType.ComponentType` ''' - return self._components[name.lower()] + lower_name = name.lower() + if lower_name in self._components: + return self._components[lower_name] + return self._procedure_components[lower_name] + + def add_procedure_component( + self, + name: str, + datatype: DataType, + visibility: Symbol.Visibility, + initial_value: Optional[DataNode] = None, + preceding_comment: str = "", + inline_comment: str = "" + ): + '''Add a type-bound procedure to this StructureType. + + The procedure is represented by the same immutable record as a data + component. Its optional initial value is a Reference to the routine + implementing the binding. + + :param name: the binding name. + :param datatype: the datatype of the binding. + :param visibility: whether this binding is public or private. + :param initial_value: reference to the bound routine, if specified. + :param preceding_comment: a comment preceding this binding. + :param inline_comment: a comment following this binding. + + :raises TypeError: if any supplied value is of the wrong type. + ''' + # Imports here avoid circular dependencies. + # pylint: disable=import-outside-toplevel + from psyclone.psyir.nodes import Reference + + if not isinstance(name, str): + raise TypeError( + f"The name of a procedure component of a StructureType must " + f"be a 'str' but got '{type(name).__name__}'") + if not isinstance(datatype, DataType): + raise TypeError( + f"The type of a procedure component of a StructureType must " + f"be a 'DataType' but got '{type(datatype).__name__}'") + if not isinstance(visibility, Symbol.Visibility): + raise TypeError( + f"The visibility of a procedure component of a StructureType " + f"must be an instance of 'Symbol.Visibility' but got " + f"'{type(visibility).__name__}'") + if (initial_value is not None and + (not isinstance(initial_value, Reference) or + not isinstance(initial_value.symbol, Symbol))): + raise TypeError( + "The initial value of a procedure component of a " + "StructureType must be None or a Reference to a " + f"Symbol but got '{type(initial_value).__name__}'.") + if not isinstance(preceding_comment, str): + raise TypeError( + "The preceding_comment of a procedure component of a " + f"StructureType must be a 'str' but got " + f"'{type(preceding_comment).__name__}'") + if not isinstance(inline_comment, str): + raise TypeError( + "The inline_comment of a procedure component of a " + f"StructureType must be a 'str' but got " + f"'{type(inline_comment).__name__}'") + + key_name = name.lower() + self._procedure_components[key_name] = self.ComponentType( + name, datatype, visibility, initial_value) + # Use object.__setattr__ due to the frozen nature of ComponentType. + object.__setattr__(self._procedure_components[key_name], + "_preceding_comment", preceding_comment) + object.__setattr__(self._procedure_components[key_name], + "_inline_comment", inline_comment) + def __eq__(self, other): ''' @@ -1420,6 +1551,12 @@ def __eq__(self, other): if self.components != other.components: return False + if self.procedure_components != other.procedure_components: + return False + + if self.extends is not other.extends: + return False + return True def replace_symbols_using(self, table_or_symbol): @@ -1469,6 +1606,28 @@ def replace_symbols_using(self, table_or_symbol): preceding_comment=component.preceding_comment, inline_comment=component.inline_comment) + for component in list(self.procedure_components.values()): + new_type = component.datatype.copy() + new_type.replace_symbols_using(table_or_symbol) + + initial_value = component.initial_value + if initial_value: + initial_value = initial_value.copy() + initial_value.replace_symbols_using(table_or_symbol) + + self.add_procedure_component( + component.name, new_type, component.visibility, + initial_value, component.preceding_comment, + component.inline_comment) + + if self.extends: + if isinstance(table_or_symbol, Symbol): + if table_or_symbol.name.lower() == self.extends.name.lower(): + self._extends = table_or_symbol + else: + self._extends = table_or_symbol.lookup( + self.extends.name, otherwise=self.extends) + def get_all_accessed_symbols(self) -> set[Symbol]: ''' :returns: a set of all the symbols accessed inside this DataType. @@ -1482,6 +1641,13 @@ def get_all_accessed_symbols(self) -> set[Symbol]: if cmpt.initial_value: symbols.update( cmpt.initial_value.get_all_accessed_symbols()) + for cmpt in self.procedure_components.values(): + symbols.update(cmpt.datatype.get_all_accessed_symbols()) + if cmpt.initial_value: + symbols.update( + cmpt.initial_value.get_all_accessed_symbols()) + if self.extends: + symbols.add(self.extends) return symbols diff --git a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py index 3f681d5083..7a9213667e 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py @@ -283,8 +283,8 @@ def test_parse_derived_type(use_stmt, type_name): @pytest.mark.usefixtures("f2008_parser") def test_derived_type_contains(): - ''' Check that we get a DataTypeSymbol of UnsupportedFortranType if a - derived-type definition has a CONTAINS section. ''' + '''Check that a derived-type CONTAINS section is captured in its + StructureType.''' fake_parent = KernelSchedule.create("dummy_schedule") symtab = fake_parent.symbol_table processor = Fparser2Reader() @@ -298,16 +298,86 @@ def test_derived_type_contains(): fparser2spec = Fortran2003.Specification_Part(reader) processor.process_declarations(fake_parent, fparser2spec.content, []) sym = symtab.lookup("my_type") - # It should still be a DataTypeSymbol but its type is unknown. assert isinstance(sym, DataTypeSymbol) - assert isinstance(sym.datatype, UnsupportedFortranType) - assert sym.datatype.declaration == '''\ -TYPE :: my_type - INTEGER :: flag - REAL, DIMENSION(3) :: posn - CONTAINS - PROCEDURE :: init => obesdv_setup -END TYPE my_type''' + assert isinstance(sym.datatype, StructureType) + assert len(sym.datatype.components) == 2 + assert list(sym.datatype.procedure_components) == ["init"] + procedure = sym.datatype.lookup("init") + assert procedure.name == "init" + assert isinstance(procedure.datatype, UnsupportedFortranType) + assert (procedure.datatype.declaration == + "PROCEDURE :: init => obesdv_setup") + assert procedure.visibility == Symbol.Visibility.PUBLIC + assert isinstance(procedure.initial_value, Reference) + assert isinstance(procedure.initial_value.symbol, RoutineSymbol) + assert procedure.initial_value.symbol.name == "obesdv_setup" + assert procedure.initial_value.symbol is symtab.lookup("obesdv_setup") + + +@pytest.mark.usefixtures("f2008_parser") +def test_derived_type_extends(): + ''' + Check that the extends attribute is handled correctly. + ''' + fake_parent = KernelSchedule.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + reader = FortranStringReader("type, extends(other_type) :: my_type\n" + " private\n" + " integer :: flag\n" + " real, public :: scale\n" + "end type my_type\n") + fparser2spec = Fortran2003.Specification_Part(reader) + processor.process_declarations(fake_parent, fparser2spec.content, []) + sym = symtab.lookup("my_type") + assert isinstance(sym, DataTypeSymbol) + assert isinstance(sym.datatype, StructureType) + assert isinstance(sym.datatype.extends, DataTypeSymbol) + assert sym.datatype.extends.name == "other_type" + assert isinstance(sym.datatype.extends.datatype, StructureType) + assert sym.datatype.extends is symtab.lookup("other_type") + + +@pytest.mark.usefixtures("f2008_parser") +def test_full_metadata_style_structures(): + '''Check that an EXTENDS attribute and type-bound procedure are captured + for a metadata-style derived type.''' + fake_parent = KernelSchedule.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + reader = FortranStringReader(""" + type, extends(kernel_type) :: compute_cu + type(go_arg), dimension(4) :: meta_args = (/ & + go_arg(go_write, go_cu, go_pointwise), & + go_arg(go_read, go_ct, go_stencil(000, 011, 000)), & + go_arg(go_read, go_grid_area_t), & + go_arg(go_read, go_r_scalar, go_pointwise) /) + integer :: iterates_over = go_all_pts + integer :: index_offset = go_offset_sw + contains + procedure, nopass :: code => compute_cu_code + end type compute_cu + """) + fparser2spec = Fortran2003.Specification_Part(reader) + processor.process_declarations(fake_parent, fparser2spec.content, []) + sym = symtab.lookup("compute_cu") + assert isinstance(sym, DataTypeSymbol) + assert isinstance(sym.datatype, StructureType) + assert isinstance(sym.datatype.extends, DataTypeSymbol) + assert sym.datatype.extends.name == "kernel_type" + assert isinstance(sym.datatype.extends.datatype, StructureType) + assert sym.datatype.extends is symtab.lookup("kernel_type") + assert list(sym.datatype.procedure_components) == ["code"] + procedure = sym.datatype.lookup("code") + assert procedure.name == "code" + assert isinstance(procedure.datatype, UnsupportedFortranType) + assert (procedure.datatype.declaration == + "PROCEDURE, NOPASS :: code => compute_cu_code") + assert procedure.visibility == Symbol.Visibility.PUBLIC + assert isinstance(procedure.initial_value, Reference) + assert isinstance(procedure.initial_value.symbol, RoutineSymbol) + assert procedure.initial_value.symbol.name == "compute_cu_code" + assert procedure.initial_value.symbol is symtab.lookup("compute_cu_code") @pytest.mark.usefixtures("f2008_parser") diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index a803598583..658986996f 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -1231,6 +1231,74 @@ def test_create_structuretype(): in str(err.value)) +def test_create_structuretype_procedures_and_extends(): + '''Test creation of a StructureType with procedure components and an + extended type.''' + target = Symbol("target") + parent_type = Symbol("parent_type") + stype = StructureType.create( + [], + [("ProC", UnresolvedType(), Symbol.Visibility.PRIVATE, + Reference(target), "Before", "After")], + extends=parent_type) + + assert stype.extends is parent_type + assert list(stype.procedure_components) == ["proc"] + # Both lookup methods are case insensitive and the general lookup method + # also searches procedure components. + proc = stype.lookup("PROC") + assert proc is stype.lookup("pRoC") + assert proc.name == "ProC" + assert isinstance(proc.datatype, UnresolvedType) + assert proc.visibility == Symbol.Visibility.PRIVATE + assert proc.initial_value.symbol is target + assert proc.preceding_comment == "Before" + assert proc.inline_comment == "After" + + with pytest.raises(TypeError) as err: + StructureType.create( + [], [("proc", UnresolvedType())]) + assert ("Each procedure component must be specified using a 3 to 6-tuple " + "of (name, type, visibility, initial_value, preceding_comment, " + "inline_comment) but found a tuple with 2 members" in + str(err.value)) + + with pytest.raises(TypeError) as err: + stype.extends = "invalid" + assert ("The type that a StructureType extends must be a Symbol but got " + "'str'." in str(err.value)) + + +@pytest.mark.parametrize( + "arguments, expected", + [ + ((1, UnresolvedType(), Symbol.Visibility.PUBLIC), + "name of a procedure component of a StructureType must be a 'str' " + "but got 'int'"), + (("proc", "invalid", Symbol.Visibility.PUBLIC), + "type of a procedure component of a StructureType must be a " + "'DataType' but got 'str'"), + (("proc", UnresolvedType(), "invalid"), + "visibility of a procedure component of a StructureType must be an " + "instance of 'Symbol.Visibility' but got 'str'"), + (("proc", UnresolvedType(), Symbol.Visibility.PUBLIC, "invalid"), + "initial value of a procedure component of a StructureType must be " + "None or a Reference to a Symbol but got 'str'"), + (("proc", UnresolvedType(), Symbol.Visibility.PUBLIC, None, None), + "preceding_comment of a procedure component of a StructureType must " + "be a 'str' but got 'NoneType'"), + (("proc", UnresolvedType(), Symbol.Visibility.PUBLIC, None, "", None), + "inline_comment of a procedure component of a StructureType must be " + "a 'str' but got 'NoneType'") + ]) +def test_structuretype_add_procedure_errors(arguments, expected): + '''Test validation when adding a procedure component.''' + stype = StructureType() + with pytest.raises(TypeError) as err: + stype.add_procedure_component(*arguments) + assert expected in str(err.value) + + def test_structuretype_eq(): '''Test the equality operator of StructureType.''' stype = StructureType.create([ @@ -1269,6 +1337,16 @@ def test_structuretype_eq(): Literal("1.0", ScalarType.real_type())), ("roger", ScalarType.integer_type(), Symbol.Visibility.PUBLIC, None)]) + procedure = [("proc", UnresolvedType(), Symbol.Visibility.PUBLIC)] + proc_stype = StructureType.create([], procedure) + assert proc_stype == StructureType.create([], procedure) + assert proc_stype != StructureType() + + parent = Symbol("parent") + extended = StructureType.create([], extends=parent) + assert extended == StructureType.create([], extends=parent) + assert extended != StructureType.create([], extends=Symbol("parent")) + @pytest.mark.parametrize("table", [None, SymbolTable()]) def test_structuretype_replace_symbols(table): @@ -1300,19 +1378,71 @@ def test_structuretype_replace_symbols(table): assert stype.components["barry"].datatype is newtsymbol +@pytest.mark.parametrize("use_table", [False, True]) +def test_structuretype_replace_procedure_and_extends(use_table): + '''Test symbol replacement in procedure components and EXTENDS.''' + original = Symbol("dependency") + proc_type = ArrayType(ScalarType.real_type(), [Reference(original)]) + stype = StructureType.create( + [], [("proc", proc_type, Symbol.Visibility.PUBLIC, + Reference(original), "Before", "After"), + ("no_target", UnresolvedType(), Symbol.Visibility.PRIVATE)], + extends=original) + + # A non-matching Symbol or an empty table must leave all references to the + # original symbol unchanged. + if use_table: + empty_table = SymbolTable() + stype.replace_symbols_using(empty_table) + else: + stype.replace_symbols_using(Symbol("unrelated")) + proc = stype.procedure_components["proc"] + assert proc.datatype.shape[0].upper.symbol is original + assert proc.initial_value.symbol is original + assert stype.procedure_components["no_target"].initial_value is None + assert stype.extends is original + + replacement = Symbol("dependency") + if use_table: + table = SymbolTable() + table.add(replacement) + stype.replace_symbols_using(table) + else: + stype.replace_symbols_using(replacement) + + proc = stype.procedure_components["proc"] + assert proc.datatype.shape[0].upper.symbol is replacement + assert proc.initial_value.symbol is replacement + assert stype.procedure_components["no_target"].initial_value is None + assert proc.preceding_comment == "Before" + assert proc.inline_comment == "After" + assert stype.extends is replacement + + def test_structuretype_get_all_accessed_symbols(): '''Tests for the get_all_accessed_symbols() method of StructureType.''' tsymbol = DataTypeSymbol("my_type", UnresolvedType()) ndim = Symbol("ndim") atype = ArrayType(ScalarType.real_type(), [Reference(ndim)]) + proc_ndim = Symbol("proc_ndim") + proc_target = Symbol("proc_target") + parent_type = Symbol("parent_type") + proc_type = ArrayType(ScalarType.real_type(), [Reference(proc_ndim)]) stype = StructureType.create([ ("fred", ScalarType.integer_type(), Symbol.Visibility.PUBLIC, None), ("george", atype, Symbol.Visibility.PRIVATE, Literal("1.0", ScalarType.real_type())), - ("barry", tsymbol, Symbol.Visibility.PUBLIC, None)]) + ("barry", tsymbol, Symbol.Visibility.PUBLIC, None)], + [("proc", proc_type, Symbol.Visibility.PUBLIC, + Reference(proc_target)), + ("no_target", UnresolvedType(), Symbol.Visibility.PRIVATE)], + extends=parent_type) dependent_symbols = stype.get_all_accessed_symbols() assert tsymbol in dependent_symbols assert ndim in dependent_symbols + assert proc_ndim in dependent_symbols + assert proc_target in dependent_symbols + assert parent_type in dependent_symbols def test_structuretype_componenttype_eq(): @@ -1350,16 +1480,25 @@ def test_structuretype_componenttype_eq(): def test_structuretype___copy__(): '''Test the __copy__ method of StructureType.''' + target = Symbol("target") + parent_type = Symbol("parent_type") stype = StructureType.create([ ("nancy", ScalarType.integer_type(), Symbol.Visibility.PUBLIC, None), ("peggy", ScalarType.real_type(), Symbol.Visibility.PRIVATE, - Literal("1.0", ScalarType.real_type()))]) + Literal("1.0", ScalarType.real_type()))], + [("proc", UnresolvedType(), Symbol.Visibility.PUBLIC, + Reference(target), "Before", "After")], extends=parent_type) copied = stype.__copy__() assert copied == stype assert copied is not stype # The components should be the same objects assert copied.components["nancy"] == stype.components["nancy"] assert copied.components["peggy"] == stype.components["peggy"] + assert copied.procedure_components["proc"] == \ + stype.procedure_components["proc"] + assert copied.procedure_components["proc"].preceding_comment == "Before" + assert copied.procedure_components["proc"].inline_comment == "After" + assert copied.extends is parent_type def test_copy_with_recursions(fortran_reader): From 576bd628c82ced586339c0c93b802717bd55e5de Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Tue, 18 Aug 2026 12:31:42 +0100 Subject: [PATCH 04/12] Add backend support for the StructureType EXTENDS and CONTAINS blocks --- src/psyclone/psyir/backend/fortran.py | 67 +++++++++++++++++-- src/psyclone/psyir/symbols/datatypes.py | 1 - .../tests/psyir/backend/fortran_test.py | 50 +++++++++++++- 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 42e8810cb2..cae5a79665 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -512,6 +512,52 @@ def gen_use(self, symbol, symbol_table): f"{renames}\n") return f"{self._nindent}use{intrinsic_str}{symbol.name}\n" + def gen_proceduredecl( + self, symbol: StructureType.ComponentType, + ) -> str: + '''Create the Fortran declaration for a type-bound procedure. + + :param symbol: the procedure component to declare. + + :returns: the Fortran procedure declaration. + + :raises VisitorError: if symbol is not a StructureType component. + :raises InternalError: if visibility is requested but is neither + PUBLIC nor PRIVATE. + ''' + if not isinstance(symbol, StructureType.ComponentType): + raise VisitorError( + "gen_proceduredecl() expects a " + "'StructureType.ComponentType' as its first argument but " + f"got '{type(symbol).__name__}'") + + result = self.gen_preceding_comments(symbol) + + if isinstance(symbol.datatype, UnsupportedFortranType): + # The original declaration preserves attributes that have no + # representation in PSyIR, such as PASS, NOPASS and DEFERRED. + result += f"{self._nindent}{symbol.datatype.declaration}" + else: + result += f"{self._nindent}procedure" + if symbol.visibility == Symbol.Visibility.PRIVATE: + result += ", private" + elif symbol.visibility == Symbol.Visibility.PUBLIC: + result += ", public" + else: + raise InternalError( + "A type-bound procedure must be either public or " + f"private but procedure '{symbol.name}' has " + f"visibility '{symbol.visibility}'") + + result += f" :: {symbol.name}" + if symbol.initial_value: + result += " => " + self._visit(symbol.initial_value) + + if symbol.inline_comment: + result += f" {self._COMMENT_PREFIX}{symbol.inline_comment}" + + return result + "\n" + def gen_vardecl(self, symbol: Union[DataSymbol, Member], include_visibility: bool = False) -> str: @@ -723,6 +769,11 @@ def gen_typedecl(self, symbol, include_visibility=True): f"gen_typedecl expects a DataTypeSymbol as argument but " f"got: '{type(symbol).__name__}'") + if isinstance(symbol.datatype, UnresolvedType): + raise VisitorError( + f"Fortran backend cannot generate code for symbol " + f"'{symbol.name}' of type '{type(symbol.datatype).__name__}'") + if isinstance(symbol.datatype, UnsupportedType): if isinstance(symbol.datatype, UnsupportedFortranType): # This is a declaration of UnsupportedType. We have to ensure @@ -740,6 +791,9 @@ def gen_typedecl(self, symbol, include_visibility=True): result += f"{self._nindent}type" + if symbol.datatype.extends: + result += f", extends({symbol.datatype.extends.name})" + if include_visibility: if symbol.visibility == Symbol.Visibility.PRIVATE: result += ", private" @@ -752,12 +806,6 @@ def gen_typedecl(self, symbol, include_visibility=True): f"type '{type(symbol.visibility).__name__}'") result += f" :: {symbol.name}\n" - if isinstance(symbol.datatype, UnresolvedType): - raise VisitorError( - f"Local Symbol '{symbol.name}' is of UnresolvedType and " - f"therefore no declaration can be created for it. Should it " - f"have an ImportInterface?") - self._depth += 1 for member in symbol.datatype.components.values(): @@ -766,6 +814,13 @@ def gen_typedecl(self, symbol, include_visibility=True): # part of a module. result += self.gen_vardecl(member, include_visibility=include_visibility) + + if symbol.datatype.procedure_components: + result += f"{self._nindent}contains\n" + self._depth += 1 + for procedure in symbol.datatype.procedure_components.values(): + result += self.gen_proceduredecl(procedure) + self._depth -= 1 self._depth -= 1 result += f"{self._nindent}end type {symbol.name}" diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index 6f1b9f91b9..f8eb453086 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -1534,7 +1534,6 @@ def add_procedure_component( object.__setattr__(self._procedure_components[key_name], "_inline_comment", inline_comment) - def __eq__(self, other): ''' :param Any other: the object to check equality to. diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py index 74fd77f8e3..a01eb25abd 100644 --- a/src/psyclone/tests/psyir/backend/fortran_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_test.py @@ -385,8 +385,8 @@ def test_gen_typedecl_validation(fortran_writer, monkeypatch): tsymbol = DataTypeSymbol("my_type", UnresolvedType()) with pytest.raises(VisitorError) as err: fortran_writer.gen_typedecl(tsymbol) - assert ("Local Symbol 'my_type' is of UnresolvedType and therefore no " - "declaration can be created for it." in str(err.value)) + assert ("Fortran backend cannot generate code for symbol 'my_type' " + "of type 'UnresolvedType'" in str(err.value)) def test_gen_typedecl_unsupported_fortran_type(fortran_writer): @@ -470,6 +470,52 @@ def test_gen_typedecl(fortran_writer): assert code.startswith("type, private :: my_type\n") +def test_gen_typedecl_extends_contains(fortran_writer): + '''Check that gen_typedecl() generates EXTENDS and type-bound procedure + declarations.''' + parent_type = DataTypeSymbol("base_type", UnresolvedType()) + initialise = Symbol("initialise_impl") + code = Symbol("code_impl") + dtype = StructureType.create( + [("flag", ScalarType.integer_type(), Symbol.Visibility.PUBLIC)], + [("initialise", UnresolvedType(), Symbol.Visibility.PRIVATE, + Reference(initialise), "Initialise the object", "binding"), + ("reset", UnresolvedType(), Symbol.Visibility.PUBLIC), + ("code", UnsupportedFortranType( + "PROCEDURE, NOPASS :: code => code_impl"), + Symbol.Visibility.PUBLIC, Reference(code))], + extends=parent_type) + tsymbol = DataTypeSymbol( + "child_type", dtype, visibility=Symbol.Visibility.PRIVATE) + tsymbol.inline_comment = "derived type" + + assert fortran_writer.gen_typedecl(tsymbol) == ( + "type, extends(base_type), private :: child_type\n" + " integer, public :: flag\n" + " contains\n" + " ! Initialise the object\n" + " procedure, private :: initialise => initialise_impl ! binding\n" + " procedure, public :: reset\n" + " PROCEDURE, NOPASS :: code => code_impl\n" + "end type child_type ! derived type\n") + + +def test_gen_proceduredecl_validation(fortran_writer): + '''Check validation performed by gen_proceduredecl().''' + with pytest.raises(VisitorError) as err: + fortran_writer.gen_proceduredecl("invalid") + assert ("gen_proceduredecl() expects a " + "'StructureType.ComponentType' as its first argument but got " + "'str'" in str(err.value)) + + procedure = StructureType.ComponentType( + "proc", UnresolvedType(), "invalid", None) + with pytest.raises(InternalError) as err: + fortran_writer.gen_proceduredecl(procedure) + assert ("type-bound procedure must be either public or private but " + "procedure 'proc' has visibility 'invalid'" in str(err.value)) + + def test_reverse_map(): '''Check that the internal _reverse_map function returns a map with the expected behaviour From 2c7ea7e4d4be96e48ba860b5a73a1a2e8eb8d4c6 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 13:23:07 +0100 Subject: [PATCH 05/12] Maintain legacy metadata parsing with StructureType --- src/psyclone/domain/gocean/kernel/psyir.py | 17 +++++++- .../lfric/kernel/lfric_kernel_metadata.py | 12 +++++- .../psyad/domain/lfric/lfric_adjoint.py | 16 +++++--- .../domain/lfric/lfric_adjoint_harness.py | 41 +++++++++++-------- .../gocean/kernel/gocean_kern_psyir_test.py | 9 ++-- .../kernel/lfric_kernel_metadata_test.py | 37 ++++++----------- .../psyad/domain/lfric/test_lfric_adjoint.py | 6 +-- .../tests/psyir/frontend/fparser2_test.py | 31 +++++++++----- .../tests/psyir/tools/call_tree_utils_test.py | 3 +- 9 files changed, 101 insertions(+), 71 deletions(-) diff --git a/src/psyclone/domain/gocean/kernel/psyir.py b/src/psyclone/domain/gocean/kernel/psyir.py index 63c0bce392..dbb6922a6f 100644 --- a/src/psyclone/domain/gocean/kernel/psyir.py +++ b/src/psyclone/domain/gocean/kernel/psyir.py @@ -20,9 +20,11 @@ from psyclone.domain.gocean import GOceanConstants from psyclone.errors import InternalError from psyclone.parse.utils import ParseError +from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.frontend.fortran import FortranReader from psyclone.psyir.nodes import Container -from psyclone.psyir.symbols import DataTypeSymbol, UnsupportedFortranType +from psyclone.psyir.symbols import ( + DataTypeSymbol, StructureType, UnsupportedFortranType) class GOceanContainer(Container): @@ -193,6 +195,19 @@ def create_from_psyir(symbol): datatype = symbol.datatype + if isinstance(datatype, StructureType): + # StructureType now supports the EXTENDS and CONTAINS syntax used + # by kernel metadata. Convert it back to Fortran while this legacy + # metadata reader still parses declarations with fparser. + declaration = FortranWriter().gen_typedecl( + symbol, include_visibility=False) + # Preserve the spelling historically produced by this legacy + # metadata class. + declaration = declaration.replace( + "go_stencil(", "GO_STENCIL(") + return GOceanKernelMetadata.create_from_fortran_string( + declaration) + if not isinstance(datatype, UnsupportedFortranType): raise InternalError( f"Expected kernel metadata to be stored in the PSyIR as " diff --git a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py index d9dd2ee30d..a326c932fc 100644 --- a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py +++ b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py @@ -45,8 +45,10 @@ from psyclone.domain.lfric.kernel.shapes_metadata import ShapesMetadata from psyclone.errors import InternalError from psyclone.parse.utils import ParseError +from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.frontend.fortran import FortranReader -from psyclone.psyir.symbols import DataTypeSymbol, UnsupportedFortranType +from psyclone.psyir.symbols import ( + DataTypeSymbol, StructureType, UnsupportedFortranType) # pylint: disable=too-many-lines # pylint: disable=too-many-instance-attributes @@ -669,6 +671,14 @@ def create_from_psyir(symbol): datatype = symbol.datatype + if isinstance(datatype, StructureType): + # StructureType now supports the EXTENDS and CONTAINS syntax used + # by kernel metadata. Convert it back to Fortran while this legacy + # metadata reader still parses declarations with fparser. + declaration = FortranWriter().gen_typedecl( + symbol, include_visibility=False) + return LFRicKernelMetadata.create_from_fortran_string(declaration) + if not isinstance(datatype, UnsupportedFortranType): raise InternalError( f"Expected kernel metadata to be stored in the PSyIR as " diff --git a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py index 971cf7c59e..d58dd61529 100644 --- a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py +++ b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py @@ -24,8 +24,9 @@ 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, UnsupportedFortranType -from psyclone.psyir.symbols.symbol import ArgumentInterface, ImportInterface +from psyclone.psyir.symbols import ContainerSymbol, StructureType +from psyclone.psyir.symbols.symbol import ( + ArgumentInterface, ImportInterface, UnresolvedInterface) # pylint: disable=too-many-locals @@ -52,8 +53,9 @@ def generate_lfric_adjoint(tl_psyir, active_variables): # linear kernel. tl_container = find_container(tl_psyir) for sym in tl_container.symbol_table.datatypesymbols: - if (isinstance(sym.datatype, UnsupportedFortranType) and - "extends(kernel_type)" in sym.datatype.declaration.lower()): + if (isinstance(sym.datatype, StructureType) and + sym.datatype.extends and + sym.datatype.extends.name.lower() == "kernel_type"): tl_metadata_name = sym.name break else: @@ -246,8 +248,12 @@ def _check_or_add_access_symbol(container, access): ''' kernel = container.children[0] symbol_table = kernel.symbol_table + arg_mod_symbol = symbol_table.find_or_create( + "argument_mod", symbol_type=ContainerSymbol) try: argument_mod_symbol = symbol_table.lookup(access) + if isinstance(argument_mod_symbol.interface, UnresolvedInterface): + argument_mod_symbol.interface = ImportInterface(arg_mod_symbol) if not isinstance(argument_mod_symbol.interface, ImportInterface): raise GenerationError( f"The existing symbol '{access}' is not imported from a use " @@ -259,8 +265,6 @@ def _check_or_add_access_symbol(container, access): f"'{argument_mod_symbol.interface.container_symbol.name}' but " f"should be imported from 'argument_mod'.") except KeyError: - arg_mod_symbol = symbol_table.find_or_create( - "argument_mod", symbol_type=ContainerSymbol) symbol_table = arg_mod_symbol.find_symbol_table(kernel) symbol_table.new_symbol( root_name=access, interface=ImportInterface(arg_mod_symbol)) diff --git a/src/psyclone/psyad/domain/lfric/lfric_adjoint_harness.py b/src/psyclone/psyad/domain/lfric/lfric_adjoint_harness.py index 640e6696cd..4ecf442d71 100644 --- a/src/psyclone/psyad/domain/lfric/lfric_adjoint_harness.py +++ b/src/psyclone/psyad/domain/lfric/lfric_adjoint_harness.py @@ -15,6 +15,7 @@ from psyclone.domain.lfric.algorithm.lfric_alg import LFRicAlg from psyclone.domain.lfric.algorithm.psyir import ( LFRicAlgorithmInvokeCall, LFRicBuiltinFunctorFactory, LFRicKernelFunctor) +from psyclone.domain.lfric.kernel import LFRicKernelMetadata from psyclone.domain.lfric.transformations import RaisePSyIR2LFRicKernTrans from psyclone.errors import InternalError, GenerationError from psyclone.psyad.domain.common.adjoint_utils import ( @@ -538,18 +539,7 @@ def generate_lfric_adjoint_harness(tl_psyir, coord_arg_idx=None, tl_subroutine_table = tl_subroutine.symbol_table tl_argument_list = tl_subroutine_table.argument_list - # Parse the kernel metadata. This still uses fparser1 as that's what - # the meta-data handling is currently based upon. We therefore have to - # convert back from PSyIR to Fortran for the moment. - # TODO #2151 - replace this with the new PSyIR-based metadata handling. - # pylint: disable=import-outside-toplevel - from psyclone.psyir.backend.fortran import FortranWriter - writer = FortranWriter() - tl_source = writer(tl_container) - parse_tree = fpapi.parse(tl_source) - - # Get the name of the module that contains the kernel and create a - # ContainerSymbol for it. + # Validate the module name before attempting to serialise its metadata. kernel_mod_name = tl_container.name.lower() if not kernel_mod_name.endswith("_mod"): raise ValueError( @@ -557,13 +547,30 @@ def generate_lfric_adjoint_harness(tl_psyir, coord_arg_idx=None, f"'{kernel_mod_name}'. This does not end in '_mod' and as such " f"does not comply with the LFRic naming convention.") - kernel_mod = table.new_symbol(kernel_mod_name, symbol_type=ContainerSymbol) - # Assume the LFRic naming convention is followed in order to infer the name - # of the TL kernel. (If this convention isn't followed in the supplied code - # then the call to `kernel_from_metadata` below will raise an appropriate - # exception.) + # Assume the LFRic naming convention is followed in order to infer the + # name of the TL kernel. kernel_name = kernel_mod_name.replace("_mod", "_type") + # Parse the kernel metadata. This still uses fparser1 as that's what + # the metadata handling is currently based upon. Serialise only the + # metadata and a placeholder implementation because unresolved metadata + # names are now visible in the language-level PSyIR. + # TODO #2151 - replace this with the new PSyIR-based metadata handling. + metadata_symbol = tl_container.symbol_table.lookup(kernel_name) + metadata = LFRicKernelMetadata.create_from_psyir(metadata_symbol) + procedure_name = metadata.procedure_name + tl_source = ( + f"module {kernel_mod_name}\n" + f"{metadata.fortran_string()}\n" + "contains\n" + f"subroutine {procedure_name}()\n" + f"end subroutine {procedure_name}\n" + f"end module {kernel_mod_name}\n") + parse_tree = fpapi.parse(tl_source) + + # Create a ContainerSymbol for the module containing the kernel. + kernel_mod = table.new_symbol(kernel_mod_name, symbol_type=ContainerSymbol) + adj_mod = table.new_symbol(create_adjoint_name(kernel_mod_name), symbol_type=ContainerSymbol) kernel_routine = table.new_symbol(kernel_name, diff --git a/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py b/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py index ff60193ba9..971c54b9bc 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py @@ -23,6 +23,7 @@ from psyclone.domain.gocean.transformations import RaisePSyIR2GOceanKernTrans from psyclone.errors import InternalError from psyclone.parse.utils import ParseError +from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.nodes import Container from psyclone.psyir.symbols import SymbolTable, ScalarType @@ -176,7 +177,7 @@ def test_goceankernelmetadata_create1(fortran_reader): _ = GOceanKernelMetadata.create_from_psyir("symbol") assert "Expected a DataTypeSymbol but found a str." in str(info.value) metadata = GOceanKernelMetadata.create_from_psyir(symbol) - assert METADATA in metadata.fortran_string() + assert METADATA.upper() in metadata.fortran_string().upper() symbol._datatype = ScalarType.real_type() with pytest.raises(InternalError) as info: _ = GOceanKernelMetadata.create_from_psyir(symbol) @@ -369,10 +370,10 @@ def test_getproperty(fortran_reader): ''' kernel_psyir = fortran_reader.psyir_from_source(PROGRAM) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "compute_cu").datatype + symbol = kernel_psyir.children[0].symbol_table.lookup("compute_cu") metadata = GOceanKernelMetadata() - reader = FortranStringReader(datatype.declaration) + reader = FortranStringReader( + FortranWriter().gen_typedecl(symbol, include_visibility=False)) spec_part = Fortran2003.Derived_Type_Def(reader) assert metadata._get_property(spec_part, "code").string == \ "compute_cu_code" diff --git a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py index 2575660376..bc36487437 100644 --- a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py @@ -10,7 +10,6 @@ ''' import pytest -from fparser.common.readfortran import FortranStringReader from fparser.two import Fortran2003 from psyclone.domain.lfric import LFRicConstants @@ -1109,28 +1108,22 @@ def test_lower_to_psyir(): assert symbol.datatype.declaration == metadata.fortran_string() -def test_get_procedure_name_error(fortran_reader): +def test_get_procedure_name_error(): '''Test that all the exceptions are raised as expected in the _get_procedure_name method. ''' - kernel_psyir = fortran_reader.psyir_from_source(PROGRAM.replace( - "procedure, nopass :: code => testkern_code", "")) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "testkern_type").datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) - spec_part = Fortran2003.Derived_Type_Def(reader) + spec_part = LFRicKernelMetadata.create_fparser2( + METADATA.replace("procedure, nopass :: code => testkern_code", ""), + Fortran2003.Derived_Type_Def) with pytest.raises(ParseError) as info: metadata._get_procedure_name(spec_part) assert "Expecting a type-bound procedure, but found" in str(info.value) - kernel_psyir = fortran_reader.psyir_from_source(PROGRAM) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "testkern_type").datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) - spec_part = Fortran2003.Derived_Type_Def(reader) + spec_part = LFRicKernelMetadata.create_fparser2( + METADATA, Fortran2003.Derived_Type_Def) binding = spec_part.children[2] binding.children[1] = binding.children[0] with pytest.raises(ParseError) as info: @@ -1138,13 +1131,10 @@ def test_get_procedure_name_error(fortran_reader): assert ("Expecting a specific binding for the type-bound procedure, " "but found" in str(info.value)) - kernel_psyir = fortran_reader.psyir_from_source(PROGRAM.replace( - "code", "node")) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "testkern_type").datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) - spec_part = Fortran2003.Derived_Type_Def(reader) + spec_part = LFRicKernelMetadata.create_fparser2( + METADATA.replace("code", "node"), + Fortran2003.Derived_Type_Def) with pytest.raises(ParseError) as info: metadata._get_procedure_name(spec_part) assert ("Expecting the type-bound procedure binding-name to be 'code' " @@ -1152,18 +1142,15 @@ def test_get_procedure_name_error(fortran_reader): in str(info.value)) -def test_get_procedure_name(fortran_reader): +def test_get_procedure_name(): '''Test utility function that takes metadata in an fparser2 tree and returns the procedure metadata name, or None is there is no procedure name. ''' - kernel_psyir = fortran_reader.psyir_from_source(PROGRAM) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "testkern_type").datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) - spec_part = Fortran2003.Derived_Type_Def(reader) + spec_part = LFRicKernelMetadata.create_fparser2( + METADATA, Fortran2003.Derived_Type_Def) assert metadata._get_procedure_name(spec_part) == \ "testkern_code" 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 e69a85734b..9fcdd48a9c 100644 --- a/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py +++ b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py @@ -248,12 +248,8 @@ def test_generate_lfric_adjoint_multi_precision( psyir = fortran_reader.psyir_from_source(tl_fortran_str) sym_table = psyir.children[0].symbol_table test_type_symbol = sym_table.lookup("test_type") - datatype = test_type_symbol.datatype # Remove procedure metadata - new_declaration = (datatype.declaration. - replace("PROCEDURE, NOPASS :: kern_code", ""). - replace("CONTAINS", "")) - datatype._declaration = new_declaration + test_type_symbol.datatype.procedure_components.clear() ad_psyir = generate_lfric_adjoint(psyir, ["field_1_w0", "field_2_w0"]) result = fortran_writer(ad_psyir) # Check that the metadata type name is updated. diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index 18d8474126..31a50a0c7d 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -2843,7 +2843,7 @@ def test_structures(fortran_reader, fortran_writer): " integer, public :: j\n" " end type my_type\n" in result) - # type that extends another type (UnsupportedFortranType) + # type that extends another type (StructureType) test_code = ( "module test_mod\n" " use kernel_mod, only : kernel_type\n" @@ -2855,14 +2855,18 @@ def test_structures(fortran_reader, fortran_writer): sym_table = psyir.children[0].symbol_table symbol = sym_table.lookup("my_type") assert isinstance(symbol, DataTypeSymbol) - assert isinstance(symbol.datatype, UnsupportedFortranType) + assert isinstance(symbol.datatype, StructureType) + assert symbol.datatype.extends is sym_table.lookup("kernel_type") + assert symbol.datatype.extends.name == "kernel_type" + assert list(symbol.datatype.components) == ["i"] + assert not symbol.datatype.procedure_components result = fortran_writer(psyir) assert ( " type, extends(kernel_type), public :: my_type\n" - " INTEGER :: i = 1\n" - "END TYPE my_type\n" in result) + " integer, public :: i = 1\n" + " end type my_type\n" in result) - # type that contains a procedure (UnsupportedFortranType) + # type that contains a procedure (StructureType) test_code = ( "module test_mod\n" " type :: test_type\n" @@ -2878,14 +2882,21 @@ def test_structures(fortran_reader, fortran_writer): sym_table = psyir.children[0].symbol_table symbol = sym_table.lookup("test_type") assert isinstance(symbol, DataTypeSymbol) - assert isinstance(symbol.datatype, UnsupportedFortranType) + assert isinstance(symbol.datatype, StructureType) + assert symbol.datatype.extends is None + assert list(symbol.datatype.components) == ["i"] + assert list(symbol.datatype.procedure_components) == ["test_code"] + procedure = symbol.datatype.lookup("test_code") + assert procedure.initial_value is None + assert (procedure.datatype.declaration == + "PROCEDURE, NOPASS :: test_code") result = fortran_writer(psyir) assert ( " type, public :: test_type\n" - " INTEGER :: i = 1\n" - " CONTAINS\n" - " PROCEDURE, NOPASS :: test_code\n" - "END TYPE test_type\n" in result) + " integer, public :: i = 1\n" + " contains\n" + " PROCEDURE, NOPASS :: test_code\n" + " end type test_type\n" in result) # type that creates an abstract type and contains a procedure # (UnsupportedFortranType) diff --git a/src/psyclone/tests/psyir/tools/call_tree_utils_test.py b/src/psyclone/tests/psyir/tools/call_tree_utils_test.py index 64b572c0d7..5ac622ba6d 100644 --- a/src/psyclone/tests/psyir/tools/call_tree_utils_test.py +++ b/src/psyclone/tests/psyir/tools/call_tree_utils_test.py @@ -337,12 +337,11 @@ def test_get_non_local_read_write_info_errors(caplog): with caplog.at_level(logging.WARNING, logger=TEST_LOGGER): ctu.get_non_local_read_write_info(schedule, rw_info) assert (f"Could not get PSyIR for Routine 'testkern_import_symbols_code' " - f"from module '{kernels[0].module_name}' as no possible" + f"from module '{kernels[0].module_name}'" in caplog.text) # Add a RoutineSymbol back into the symbol table to mimic a CodeBlock # representing the routine. - cntr.symbol_table.add(RoutineSymbol("testkern_import_symbols_code")) rw_info = ReadWriteInfo() with caplog.at_level(logging.WARNING, logger=TEST_LOGGER): ctu.get_non_local_read_write_info(schedule, rw_info) From 227e3afe68f979d1c2f68fc70781e04801e6dfb0 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 14:35:13 +0100 Subject: [PATCH 06/12] #2642 Add TODOs --- src/psyclone/domain/gocean/kernel/psyir.py | 8 +++----- src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/psyclone/domain/gocean/kernel/psyir.py b/src/psyclone/domain/gocean/kernel/psyir.py index dbb6922a6f..eae2d34627 100644 --- a/src/psyclone/domain/gocean/kernel/psyir.py +++ b/src/psyclone/domain/gocean/kernel/psyir.py @@ -196,13 +196,11 @@ def create_from_psyir(symbol): datatype = symbol.datatype if isinstance(datatype, StructureType): - # StructureType now supports the EXTENDS and CONTAINS syntax used - # by kernel metadata. Convert it back to Fortran while this legacy - # metadata reader still parses declarations with fparser. + # TODO #239: GOceanKernelMetadata.create_from_psyir will + # replace this declaration = FortranWriter().gen_typedecl( symbol, include_visibility=False) - # Preserve the spelling historically produced by this legacy - # metadata class. + # Preserve the spelling declaration = declaration.replace( "go_stencil(", "GO_STENCIL(") return GOceanKernelMetadata.create_from_fortran_string( diff --git a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py index a326c932fc..efddbb4210 100644 --- a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py +++ b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py @@ -672,9 +672,8 @@ def create_from_psyir(symbol): datatype = symbol.datatype if isinstance(datatype, StructureType): - # StructureType now supports the EXTENDS and CONTAINS syntax used - # by kernel metadata. Convert it back to Fortran while this legacy - # metadata reader still parses declarations with fparser. + # TODO #239: LFricKernelMetadata.create_from_psyir will + # replace this declaration = FortranWriter().gen_typedecl( symbol, include_visibility=False) return LFRicKernelMetadata.create_from_fortran_string(declaration) From 369e64dd89463f5e781a4914defb0ec60eb3fbba Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 15:00:56 +0100 Subject: [PATCH 07/12] #2642 Improve test coverage and add typehints --- src/psyclone/domain/gocean/kernel/psyir.py | 15 +++------ .../lfric/kernel/lfric_kernel_metadata.py | 14 +++------ src/psyclone/psyir/symbols/datatypes.py | 31 +++++++------------ .../gocean/kernel/gocean_kern_psyir_test.py | 2 +- .../kernel/lfric_kernel_metadata_test.py | 2 +- 5 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/psyclone/domain/gocean/kernel/psyir.py b/src/psyclone/domain/gocean/kernel/psyir.py index eae2d34627..f1681acd08 100644 --- a/src/psyclone/domain/gocean/kernel/psyir.py +++ b/src/psyclone/domain/gocean/kernel/psyir.py @@ -206,16 +206,11 @@ def create_from_psyir(symbol): return GOceanKernelMetadata.create_from_fortran_string( declaration) - if not isinstance(datatype, UnsupportedFortranType): - raise InternalError( - f"Expected kernel metadata to be stored in the PSyIR as " - f"an UnsupportedFortranType, but found " - f"{type(datatype).__name__}.") - - # In an UnsupportedFortranType, the declaration is stored as a - # string, so use create_from_fortran_string() - return GOceanKernelMetadata.create_from_fortran_string( - datatype.declaration) + raise InternalError( + f"Expected kernel metadata to be stored in the PSyIR as " + f"an StructureType, but found " + f"{type(datatype).__name__}.") + @staticmethod def create_from_fortran_string(fortran_string): diff --git a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py index efddbb4210..605a8903a3 100644 --- a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py +++ b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py @@ -678,16 +678,10 @@ def create_from_psyir(symbol): symbol, include_visibility=False) return LFRicKernelMetadata.create_from_fortran_string(declaration) - if not isinstance(datatype, UnsupportedFortranType): - raise InternalError( - f"Expected kernel metadata to be stored in the PSyIR as " - f"an UnsupportedFortranType, but found " - f"{type(datatype).__name__}.") - - # In an UnsupportedFortranType, the declaration is stored as a - # string, so use create_from_fortran_string() - return LFRicKernelMetadata.create_from_fortran_string( - datatype.declaration) + raise InternalError( + f"Expected kernel metadata to be stored in the PSyIR as " + f"an StructureType, but found " + f"{type(datatype).__name__}.") @staticmethod def create_from_fparser2(fparser2_tree): diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index f8eb453086..e05c59852d 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -1290,31 +1290,26 @@ def __copy__(self): return new @staticmethod - def create(components, procedure_components=None, extends=None): + def create( + components: list[tuple[ + str, Union[DataType, DataTypeSymbol], Symbol.Visibility, + Optional[DataNode], Optional[str], Optional[str]]], + procedure_components: Optional[list[tuple[ + str, Union[DataType, DataTypeSymbol], Symbol.Visibility, + Optional[DataNode], Optional[str], Optional[str]]]] = None, + extends: Optional[DataTypeSymbol] = None + ) -> 'StructureType': ''' Creates a StructureType from the supplied list of properties. :param components: the name, type, visibility (whether public or private), initial value (if any), preceding comment (if any) and inline comment (if any) of each component. - :type components: List[tuple[ - str, - :py:class:`psyclone.psyir.symbols.DataType` | - :py:class:`psyclone.psyir.symbols.DataTypeSymbol`, - :py:class:`psyclone.psyir.symbols.Symbol.Visibility`, - Optional[:py:class:`psyclone.psyir.symbols.DataNode`], - Optional[str], - Optional[str] - ]] :param procedure_components: the procedure bindings of this type, specified in the same way as data components. - :type procedure_components: Optional[List[tuple]] :param extends: the type extended by this type, if any. - :type extends: Optional[ - :py:class:`psyclone.psyir.symbols.DataTypeSymbol`] :returns: the new type object. - :rtype: :py:class:`psyclone.psyir.symbols.StructureType` ''' stype = StructureType() @@ -1455,11 +1450,10 @@ def add(self, name: str, datatype, visibility, initial_value=None, "_inline_comment", inline_comment) - def lookup(self, name): + def lookup(self, name) -> 'StructureType.ComponentType': ''' :returns: the ComponentType tuple describing the named member of this StructureType. - :rtype: :py:class:`psyclone.psyir.symbols.StructureType.ComponentType` ''' lower_name = name.lower() if lower_name in self._components: @@ -1534,12 +1528,11 @@ def add_procedure_component( object.__setattr__(self._procedure_components[key_name], "_inline_comment", inline_comment) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: ''' - :param Any other: the object to check equality to. + :param other: the object to check equality to. :returns: whether this StructureType is equal to the 'other' type. - :rtype: bool ''' if not super().__eq__(other): return False diff --git a/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py b/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py index 971c54b9bc..49821d477c 100644 --- a/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py +++ b/src/psyclone/tests/domain/gocean/kernel/gocean_kern_psyir_test.py @@ -182,7 +182,7 @@ def test_goceankernelmetadata_create1(fortran_reader): with pytest.raises(InternalError) as info: _ = GOceanKernelMetadata.create_from_psyir(symbol) assert ("Expected kernel metadata to be stored in the PSyIR as an " - "UnsupportedFortranType, but found ScalarType." in str(info.value)) + "StructureType, but found ScalarType." in str(info.value)) # create_from_fortran_string diff --git a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py index bc36487437..f51b391f38 100644 --- a/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py +++ b/src/psyclone/tests/domain/lfric/kernel/lfric_kernel_metadata_test.py @@ -980,7 +980,7 @@ def test_create_from_psyir_error(): _ = LFRicKernelMetadata.create_from_psyir( DataTypeSymbol("x", ScalarType.real_type())) assert ("Expected kernel metadata to be stored in the PSyIR as an " - "UnsupportedFortranType, but found ScalarType." in str(info.value)) + "StructureType, but found ScalarType." in str(info.value)) @pytest.mark.parametrize("procedure_format", ["", "code =>"]) From d64cdd81ef9b2a9e57a807d570fad724b01b3ecc Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 15:13:20 +0100 Subject: [PATCH 08/12] #2642 Add test --- .../frontend/fparser2_derived_type_test.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py index 7a9213667e..e9c0e1d0bc 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py @@ -338,6 +338,52 @@ def test_derived_type_extends(): assert sym.datatype.extends is symtab.lookup("other_type") +@pytest.mark.usefixtures("f2008_parser") +def test_derived_type_extends_and_binding_existing_symbols(): + '''Check that an existing type used by EXTENDS is reused and that an + imported generic symbol used as a binding target is specialised. + ''' + fake_parent = KernelSchedule.create("dummy_schedule") + symtab = fake_parent.symbol_table + processor = Fparser2Reader() + reader = FortranStringReader( + "use some_mod, only : initialise_impl\n" + "type :: base_type\n" + "end type base_type\n" + "type, extends(base_type) :: child_type\n" + " ! Component comment\n" + " integer :: flag\n" + "contains\n" + " private\n" + " procedure, public :: initialise => initialise_impl\n" + " procedure :: reset\n" + "end type child_type\n", + ignore_comments=False) + fparser2spec = Fortran2003.Specification_Part(reader) + processor.process_declarations(fake_parent, fparser2spec.content, []) + + dtype = symtab.lookup("child_type").datatype + parent_type = dtype.extends + assert isinstance(parent_type, DataTypeSymbol) + assert isinstance(parent_type.datatype, StructureType) + assert parent_type is symtab.lookup("base_type") + + flag = dtype.lookup("flag") + assert flag.preceding_comment == "Component comment" + + initialise = dtype.lookup("initialise") + assert initialise.visibility == Symbol.Visibility.PUBLIC + target = initialise.initial_value.symbol + assert isinstance(target, RoutineSymbol) + assert isinstance(target.datatype, UnresolvedType) + assert isinstance(target.interface, ImportInterface) + assert target is symtab.lookup("initialise_impl") + + reset = dtype.lookup("reset") + assert reset.visibility == Symbol.Visibility.PRIVATE + assert reset.initial_value is None + + @pytest.mark.usefixtures("f2008_parser") def test_full_metadata_style_structures(): '''Check that an EXTENDS attribute and type-bound procedure are captured From 9b7e14e5257bef07c54623a737dda1676c76185a Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 15:16:42 +0100 Subject: [PATCH 09/12] #2642 Fix flake8 --- src/psyclone/domain/gocean/kernel/psyir.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/psyclone/domain/gocean/kernel/psyir.py b/src/psyclone/domain/gocean/kernel/psyir.py index f1681acd08..0714e325f6 100644 --- a/src/psyclone/domain/gocean/kernel/psyir.py +++ b/src/psyclone/domain/gocean/kernel/psyir.py @@ -211,7 +211,6 @@ def create_from_psyir(symbol): f"an StructureType, but found " f"{type(datatype).__name__}.") - @staticmethod def create_from_fortran_string(fortran_string): '''Create a new instance of GOceanKernelMetadata populated with From 44a7b8fe6ba9158dc259159e3e9f22c254570751 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 15:56:06 +0100 Subject: [PATCH 10/12] #2642 Improve test coverage --- .../frontend/fparser2_derived_type_test.py | 22 +++++++++++ .../tests/psyir/tools/call_tree_utils_test.py | 37 ++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py index e9c0e1d0bc..5238f290cf 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py @@ -475,6 +475,28 @@ def test_derived_type_accessibility(): assert scale.visibility == Symbol.Visibility.PUBLIC +@pytest.mark.usefixtures("f2008_parser") +def test_unsupported_derived_type_child(): + '''Check that an unsupported child of a derived-type definition causes + the whole definition to be captured as an UnsupportedFortranType. + ''' + fake_parent = KernelSchedule.create("dummy_schedule") + processor = Fparser2Reader() + reader = FortranStringReader("type :: my_type\n" + " sequence\n" + " integer :: flag\n" + "end type my_type\n") + fparser2spec = Fortran2003.Specification_Part(reader) + processor.process_declarations(fake_parent, fparser2spec.content, []) + + datatype = fake_parent.symbol_table.lookup("my_type").datatype + assert isinstance(datatype, UnsupportedFortranType) + assert datatype.declaration == ("TYPE :: my_type\n" + " SEQUENCE\n" + " INTEGER :: flag\n" + "END TYPE my_type") + + def test_derived_type_ref(f2008_parser, fortran_writer): ''' Check that the frontend handles references to a member of a derived type. ''' diff --git a/src/psyclone/tests/psyir/tools/call_tree_utils_test.py b/src/psyclone/tests/psyir/tools/call_tree_utils_test.py index 5ac622ba6d..d2dcddbd08 100644 --- a/src/psyclone/tests/psyir/tools/call_tree_utils_test.py +++ b/src/psyclone/tests/psyir/tools/call_tree_utils_test.py @@ -332,16 +332,6 @@ def test_get_non_local_read_write_info_errors(caplog): routine = cntr.find_routine_psyir("testkern_import_symbols_code") # Remove the kernel routine from the PSyIR. routine.detach() - - rw_info = ReadWriteInfo() - with caplog.at_level(logging.WARNING, logger=TEST_LOGGER): - ctu.get_non_local_read_write_info(schedule, rw_info) - assert (f"Could not get PSyIR for Routine 'testkern_import_symbols_code' " - f"from module '{kernels[0].module_name}'" - in caplog.text) - - # Add a RoutineSymbol back into the symbol table to mimic a CodeBlock - # representing the routine. rw_info = ReadWriteInfo() with caplog.at_level(logging.WARNING, logger=TEST_LOGGER): ctu.get_non_local_read_write_info(schedule, rw_info) @@ -356,6 +346,33 @@ def test_get_non_local_read_write_info_errors(caplog): in caplog.text) +@pytest.mark.usefixtures("clear_module_manager_instance") +def test_get_non_local_read_write_info_no_possible_routines(caplog, + monkeypatch): + '''Test the handling of a kernel for which routine resolution finds no + possible routines in the module PSyIR. + ''' + Config.get().api = "lfric" + test_file = os.path.join("driver_creation", "module_with_builtin_mod.f90") + psyir, _ = get_invoke(test_file, "lfric", 0, dist_mem=False) + schedule = psyir.invokes.invoke_list[0].schedule + kernel = schedule.walk(Kern)[0] + + mod_man = ModuleManager.get() + mod_man.add_search_path(os.path.join(get_base_path("lfric"), + "driver_creation")) + cntr = mod_man.get_module_info(kernel.module_name).get_psyir() + monkeypatch.setattr(cntr, "resolve_routine", lambda _: []) + + with caplog.at_level(logging.WARNING, logger=TEST_LOGGER): + CallTreeUtils().get_non_local_read_write_info( + schedule, ReadWriteInfo()) + + assert (f"Could not get PSyIR for Routine '{kernel.name}' from module " + f"'{kernel.module_name}' as no possible routines were found - " + "ignored." in caplog.text) + + # ----------------------------------------------------------------------------- @pytest.mark.usefixtures("clear_module_manager_instance") def test_call_tree_utils_resolve_calls_unknowns(caplog): From b72c2dbe369d852fa92be30dbdba7dcc0f2ea3d5 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 16:08:58 +0100 Subject: [PATCH 11/12] #2642 Revert module_inline change --- .../common/transformations/kernel_module_inline_trans.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py index ceb8794e89..75b36d4549 100644 --- a/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py +++ b/src/psyclone/domain/common/transformations/kernel_module_inline_trans.py @@ -411,7 +411,7 @@ def apply(self, # collisions. new_sym.visibility = Symbol.Visibility.PRIVATE # Add the new symbol to the map. - name_map[code_to_inline.name.lower()] = new_sym + name_map[code_to_inline.name] = new_sym # Add the routine code into this Container code_to_inline = code_to_inline.detach() code_to_inline.symbol = new_sym @@ -425,7 +425,7 @@ def apply(self, symbol_type=GenericInterfaceSymbol, routines=[(sym, True) for sym in name_map.values()], visibility=Symbol.Visibility.PRIVATE) - name_map[interface_sym.name.lower()] = new_sym + name_map[interface_sym.name] = new_sym if update_all: # We will update all Calls/Kernels associated with the @@ -435,11 +435,11 @@ def apply(self, # Only update the supplied Call/Kernel. all_calls = [node] - target_sym = name_map.get(caller_name.lower(), None) + target_sym = name_map.get(caller_name, None) if not target_sym: # If we haven't copied in a routine of 'caller_name' then it must # be because the target of the call is renamed on import. - target_sym = name_map.get(external_callee_name.lower()) + target_sym = name_map.get(external_callee_name) for call in all_calls: name = call.routine.symbol.name.lower() From 7105ac1cc54b40cbca5c3bf0a31b2b6993993a04 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 20 Aug 2026 16:15:26 +0100 Subject: [PATCH 12/12] #2642 Add more typehints --- src/psyclone/psyir/symbols/datatypes.py | 29 +++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index e05c59852d..d4ee4ce0d9 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -1336,28 +1336,24 @@ def create( return stype @property - def components(self): + def components(self) -> dict[str, 'StructureType.ComponentType']: ''' :returns: Ordered dictionary of the components of this type. - :rtype: :py:class:`collections.OrderedDict` ''' return self._components @property - def procedure_components(self): + def procedure_components(self) -> dict[str, 'StructureType.ComponentType']: ''' :returns: ordered dictionary of the type-bound procedures of this type. - :rtype: :py:class:`collections.OrderedDict` ''' return self._procedure_components @property - def extends(self): + def extends(self) -> Optional[Symbol]: ''' :returns: the type extended by this type, or None. - :rtype: Optional[ - :py:class:`psyclone.psyir.symbols.DataTypeSymbol`] ''' return self._extends @@ -1375,21 +1371,23 @@ def extends(self, value: Symbol): f"Symbol but got '{type(value).__name__}'.") self._extends = value - def add(self, name: str, datatype, visibility, initial_value=None, - preceding_comment: str = "", inline_comment: str = ""): + def add( + self, + name: str, + datatype: Union[DataType, DataTypeSymbol], + visibility: Symbol.Visibility, + initial_value: Optional[DataNode] = None, + preceding_comment: str = "", + inline_comment: str = "" + ): ''' Create a component with the supplied attributes and add it to this StructureType. :param name: the name of the new component. :param datatype: the type of the new component. - :type datatype: :py:class:`psyclone.psyir.symbols.DataType` | - :py:class:`psyclone.psyir.symbols.DataTypeSymbol` :param visibility: whether this component is public or private. - :type visibility: :py:class:`psyclone.psyir.symbols.Symbol.Visibility` :param initial_value: the initial value of the new component. - :type initial_value: Optional[ - :py:class:`psyclone.psyir.nodes.DataNode`] :param preceding_comment: a comment that precedes this component. :param inline_comment: a comment that follows this component on the same line. @@ -1397,8 +1395,7 @@ def add(self, name: str, datatype, visibility, initial_value=None, :raises TypeError: if any of the supplied values are of the wrong type. ''' - # This import must be placed here to avoid circular - # dependencies. + # This import must be placed here to avoid circular dependencies. # pylint: disable=import-outside-toplevel from psyclone.psyir.nodes import DataNode if not isinstance(name, str):