diff --git a/doc/developer_guide/psyir_symbols.rst b/doc/developer_guide/psyir_symbols.rst index bce982c430..557b4ca9a2 100644 --- a/doc/developer_guide/psyir_symbols.rst +++ b/doc/developer_guide/psyir_symbols.rst @@ -78,16 +78,15 @@ explicitly listed may be assumed to be unsupported): +----------------------+--------------------+--------------------+ | |Supported |Unsupported | +======================+====================+====================+ -|Variables |ALLOCATABLE |CLASS | +|Variables |ALLOCATABLE, CLASS | | +----------------------+--------------------+--------------------+ | |CHARACTER, DOUBLE |COMPLEX, CHARACTER | | |PRECISION, INTEGER, |with LEN or KIND | | |LOGICAL, REAL | | +----------------------+--------------------+--------------------+ -| |Derived Types |'extends', | -| | |'abstract' or with | -| | |CONTAINS; Operator | -| | |overloading | +| |Derived Types, |'abstract', | +| |'extends', |operator overloading| +| |CONTAINS | | +----------------------+--------------------+--------------------+ | |DIMENSION |Array extents | | | |specified using | diff --git a/src/psyclone/domain/gocean/kernel/psyir.py b/src/psyclone/domain/gocean/kernel/psyir.py index 03c9b0014d..73145bbdd9 100644 --- a/src/psyclone/domain/gocean/kernel/psyir.py +++ b/src/psyclone/domain/gocean/kernel/psyir.py @@ -50,8 +50,10 @@ from psyclone.errors import InternalError from psyclone.parse.utils import ParseError from psyclone.psyir.frontend.fortran import FortranReader +from psyclone.psyir.backend.fortran import FortranWriter from psyclone.psyir.nodes import Container -from psyclone.psyir.symbols import DataTypeSymbol, UnsupportedFortranType +from psyclone.psyir.symbols import DataTypeSymbol, UnsupportedFortranType, \ + StructureType class GOceanContainer(Container): @@ -222,16 +224,22 @@ def create_from_psyir(symbol): datatype = symbol.datatype - if not isinstance(datatype, UnsupportedFortranType): + if not isinstance(datatype, StructureType): raise InternalError( f"Expected kernel metadata to be stored in the PSyIR as " - f"an UnsupportedFortranType, but found " + f"a StructureType, but found " f"{type(datatype).__name__}.") - # In an UnsupportedFortranType, the declaration is stored as a - # string, so use create_from_fortran_string() + # TODO #2643: This is a temporary solution using FortranWriter + # to allow the current metadata extraction to work with StructureType, + # instead of relying on UnsupportedFortranType. + # This will be removed when the metadata is extracted from the PSyIR + # itself. + type_declaration = FortranWriter().gen_typedecl(symbol) + type_declaration = type_declaration.replace(", public", "") + type_declaration = type_declaration.replace(", private", "") return GOceanKernelMetadata.create_from_fortran_string( - datatype.declaration) + type_declaration) @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 153e19f003..e66ae1a00c 100644 --- a/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py +++ b/src/psyclone/domain/lfric/kernel/lfric_kernel_metadata.py @@ -44,38 +44,40 @@ from fparser.two.utils import walk, get_child from psyclone.domain.lfric import LFRicConstants -from psyclone.domain.lfric.kernel.columnwise_operator_arg_metadata import \ - ColumnwiseOperatorArgMetadata +from psyclone.domain.lfric.kernel.columnwise_operator_arg_metadata import ( + ColumnwiseOperatorArgMetadata) from psyclone.domain.lfric.kernel.field_arg_metadata import FieldArgMetadata -from psyclone.domain.lfric.kernel.field_vector_arg_metadata import \ - FieldVectorArgMetadata -from psyclone.domain.lfric.kernel.inter_grid_arg_metadata import \ - InterGridArgMetadata -from psyclone.domain.lfric.kernel.inter_grid_vector_arg_metadata import \ - InterGridVectorArgMetadata -from psyclone.domain.lfric.kernel.operator_arg_metadata import \ - OperatorArgMetadata +from psyclone.domain.lfric.kernel.field_vector_arg_metadata import ( + FieldVectorArgMetadata) +from psyclone.domain.lfric.kernel.inter_grid_arg_metadata import ( + InterGridArgMetadata) +from psyclone.domain.lfric.kernel.inter_grid_vector_arg_metadata import ( + InterGridVectorArgMetadata) +from psyclone.domain.lfric.kernel.operator_arg_metadata import ( + OperatorArgMetadata) from psyclone.domain.lfric.kernel.common_metadata import CommonMetadata -from psyclone.domain.lfric.kernel.common_meta_arg_metadata import \ - CommonMetaArgMetadata -from psyclone.domain.lfric.kernel.evaluator_targets_metadata import \ - EvaluatorTargetsMetadata -from psyclone.domain.lfric.kernel.meta_args_metadata import \ - MetaArgsMetadata -from psyclone.domain.lfric.kernel.meta_funcs_metadata import \ - MetaFuncsMetadata -from psyclone.domain.lfric.kernel.meta_mesh_metadata import \ - MetaMeshMetadata -from psyclone.domain.lfric.kernel.meta_ref_element_metadata import \ - MetaRefElementMetadata -from psyclone.domain.lfric.kernel.operates_on_metadata import \ - OperatesOnMetadata +from psyclone.domain.lfric.kernel.common_meta_arg_metadata import ( + CommonMetaArgMetadata) +from psyclone.domain.lfric.kernel.evaluator_targets_metadata import ( + EvaluatorTargetsMetadata) +from psyclone.domain.lfric.kernel.meta_args_metadata import ( + MetaArgsMetadata) +from psyclone.domain.lfric.kernel.meta_funcs_metadata import ( + MetaFuncsMetadata) +from psyclone.domain.lfric.kernel.meta_mesh_metadata import ( + MetaMeshMetadata) +from psyclone.domain.lfric.kernel.meta_ref_element_metadata import ( + MetaRefElementMetadata) +from psyclone.domain.lfric.kernel.operates_on_metadata import ( + OperatesOnMetadata) from psyclone.domain.lfric.kernel.scalar_arg_metadata import ScalarArgMetadata from psyclone.domain.lfric.kernel.shapes_metadata import ShapesMetadata from psyclone.errors import InternalError from psyclone.parse.utils import ParseError from psyclone.psyir.frontend.fortran import FortranReader -from psyclone.psyir.symbols import DataTypeSymbol, UnsupportedFortranType +from psyclone.psyir.backend.fortran import FortranWriter +from psyclone.psyir.symbols import (DataTypeSymbol, UnsupportedFortranType, + StructureType) # pylint: disable=too-many-lines # pylint: disable=too-many-instance-attributes @@ -698,6 +700,16 @@ def create_from_psyir(symbol): datatype = symbol.datatype + # TODO #2643: This is a temporary solution using FortranWriter + # to allow the current metadata extraction to work with StructureType, + # instead of relying on UnsupportedFortranType. + # This will be removed when the metadata is extracted from the PSyIR + # itself. + if isinstance(datatype, StructureType): + type_declaration = FortranWriter().gen_typedecl(symbol, False) + return LFRicKernelMetadata.create_from_fortran_string( + type_declaration) + if not isinstance(datatype, UnsupportedFortranType): raise InternalError( f"Expected kernel metadata to be stored in the PSyIR as " diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py index d87f7c153f..7e561bba95 100644 --- a/src/psyclone/psyGen.py +++ b/src/psyclone/psyGen.py @@ -60,7 +60,8 @@ UnresolvedType, ImportInterface, INTEGER_TYPE, RoutineSymbol, Symbol) -from psyclone.psyir.symbols.datatypes import UnsupportedFortranType +from psyclone.psyir.symbols.datatypes import (UnsupportedFortranType, + StructureType) # The types of 'intent' that an argument to a Fortran subroutine # may have @@ -1753,6 +1754,8 @@ def _rename_psyir(self, suffix): # the kernel metadata. container_table = container.symbol_table for sym in container_table.datatypesymbols: + # Either the DataTypeSymbol is of UnsupportedFortranType, + # in which case we replace in its whole declaration. if isinstance(sym.datatype, UnsupportedFortranType): new_declaration = sym.datatype.declaration.replace( orig_kern_name, new_kern_name) @@ -1761,6 +1764,15 @@ def _rename_psyir(self, suffix): new_declaration, partial_datatype=sym.datatype.partial_datatype) # pylint: enable=protected-access + # Or the DataTypeSymbol is a StructureType, in which case we + # replace the "code" procedure component initial value. + elif isinstance(sym.datatype, StructureType): + new_kernel_symbol = container_table.lookup(new_kern_name) + new_initial_value = Reference(new_kernel_symbol) + sym.datatype.replace_procedure_component_initial_value( + orig_kern_name, + new_initial_value + ) @property def modified(self): diff --git a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py index 6a7fd95c87..73afb7a162 100644 --- a/src/psyclone/psyad/domain/lfric/lfric_adjoint.py +++ b/src/psyclone/psyad/domain/lfric/lfric_adjoint.py @@ -52,7 +52,7 @@ 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 import ContainerSymbol, StructureType from psyclone.psyir.symbols.symbol import ArgumentInterface, ImportInterface @@ -80,8 +80,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 is not None and + sym.datatype.extends.name.lower() == "kernel_type"): tl_metadata_name = sym.name break else: diff --git a/src/psyclone/psyir/backend/fortran.py b/src/psyclone/psyir/backend/fortran.py index 743352628f..a7650aab7a 100644 --- a/src/psyclone/psyir/backend/fortran.py +++ b/src/psyclone/psyir/backend/fortran.py @@ -122,11 +122,15 @@ def gen_datatype(datatype, name): ''' if isinstance(datatype, DataTypeSymbol): # Symbol is of derived type + if datatype.is_class: + return f"class({datatype.name})" return f"type({datatype.name})" if (isinstance(datatype, ArrayType) and isinstance(datatype.intrinsic, DataTypeSymbol)): # Symbol is an array of derived types + if datatype.intrinsic.is_class: + return f"class({datatype.intrinsic.name})" return f"type({datatype.intrinsic.name})" try: @@ -488,6 +492,54 @@ def gen_use(self, symbol, symbol_table): return f"{self._nindent}use{intrinsic_str}{symbol.name}\n" + def gen_proceduredecl(self, symbol, include_visibility=True): + '''Create and return the Fortran procedure declaration for this Symbol. + + :param symbol: the symbol instance. + :type symbol: Union[:py:class:`psyclone.psyir.symbols.DataSymbol`, + :py:class:`psyclone.psyir.symbols.datatypes. + StructureType.ComponentType`] + :param bool include_visibility: whether to include the visibility of + the symbol in the generated declaration (default True). + + :returns: the Fortran procedure declaration as a string. + :rtype: str + + :raises VisitorError: if the symbol is not a RoutineSymbol or a + StructureType.ComponentType. + :raises InternalError: if the visibility is to be included but is + neither PUBLIC nor PRIVATE. + ''' + if not isinstance(symbol, (DataSymbol, StructureType.ComponentType)): + raise VisitorError( + f"gen_proceduredecl() expects a 'DataSymbol' or " + f"'StructureType.ComponentType' as its first " + f"argument but got '{type(symbol).__name__}'") + + if isinstance(symbol.datatype, UnsupportedFortranType): + return f"{self._nindent}{symbol.datatype.declaration}\n" + + result = f"{self._nindent}procedure" + + if include_visibility: + if symbol.visibility == Symbol.Visibility.PRIVATE: + result += ", private" + elif symbol.visibility == Symbol.Visibility.PUBLIC: + result += ", public" + else: + raise InternalError( + f"A Symbol must be either public or private but symbol " + f"'{symbol.name}' has visibility '{symbol.visibility}'") + + # Specify name + result += f" :: {symbol.name}" + + # Specify initialisation expression + if symbol.initial_value: + result += " => " + self._visit(symbol.initial_value) + + return result + "\n" + def gen_vardecl(self, symbol, include_visibility=False): '''Create and return the Fortran variable declaration for this Symbol or derived-type member. @@ -696,8 +748,17 @@ def gen_typedecl(self, symbol, include_visibility=True): f"Fortran backend cannot generate code for symbol " f"'{symbol.name}' of type '{type(symbol.datatype).__name__}'") + if not isinstance(symbol.datatype, StructureType): + raise VisitorError( + f"gen_typedecl expects a DataTypeSymbol with a StructureType " + f"as its datatype but got: '{type(symbol.datatype).__name__}'") + result = f"{self._nindent}type" + if symbol.datatype.extends: + # This is a component of a derived type + result += f", extends({symbol.datatype.extends.name})" + if include_visibility: if symbol.visibility == Symbol.Visibility.PRIVATE: result += ", private" @@ -710,12 +771,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(): @@ -724,6 +779,14 @@ def gen_typedecl(self, symbol, include_visibility=True): # part of a module. result += self.gen_vardecl(member, include_visibility=include_visibility) + if len(symbol.datatype.procedure_components) > 0: + result += f"{self._nindent}contains\n" + self._depth += 1 + for procedure in symbol.datatype.procedure_components.values(): + result += self.gen_proceduredecl(procedure, + include_visibility) + self._depth -= 1 + self._depth -= 1 result += f"{self._nindent}end type {symbol.name}\n" @@ -937,6 +1000,9 @@ def gen_decls(self, symbol_table, is_module_scope=False): # We ignore all symbols with a PreprocessorInterface if isinstance(sym.interface, PreprocessorInterface): all_symbols.remove(sym) + # We remove the '*' symbol used in 'class(*) :: var' + if sym.name == "*": + all_symbols.remove(sym) # If the symbol table contains any symbols with an # UnresolvedInterface interface (they are not explicitly diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index 5dbb215da3..94a306808d 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1612,13 +1612,12 @@ def _process_type_spec(self, parent, type_spec): elif isinstance(type_spec, Fortran2003.Declaration_Type_Spec): # This is a variable of derived type - if type_spec.children[0].lower() != "type": - # We don't yet support declarations that use 'class' - # TODO #1504 extend the PSyIR for this variable type. - raise NotImplementedError( - f"Could not process {type_spec} - declarations " - f"other than 'type' are not yet supported.") - type_name = str(walk(type_spec, Fortran2003.Type_Name)[0]) + if isinstance(type_spec.items[1], Fortran2003.Type_Name): + type_name = str(type_spec.items[1].string).lower() + else: + # If we are processing a `class(*) :: var` declaration, the + # type name is not a Type_Name but a string. + type_name = type_spec.items[1].lower() # Do we already have a Symbol for this derived type? type_symbol = _find_or_create_unresolved_symbol(parent, type_name) # pylint: disable=unidiomatic-typecheck @@ -1636,6 +1635,8 @@ def _process_type_spec(self, parent, type_spec): f"Search for a DataTypeSymbol named '{type_name}' " f"(required by specification '{type_spec}') found a " f"'{type(type_symbol).__name__}' instead.") + if type_spec.children[0].lower() == "class": + type_symbol.is_class = True base_type = type_symbol else: @@ -1956,6 +1957,7 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): # Look for any private-components-stmt (R447) within the type # decln. In the absence of this, the default visibility of type # components is public. + # This only concerns data components, i.e. not procedure ones. private_stmts = walk(decl, Fortran2003.Private_Components_Stmt) if private_stmts: default_compt_visibility = Symbol.Visibility.PRIVATE @@ -2002,21 +2004,48 @@ 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 + # We support derived-types with additional attribute "extends" + # only. + # We do not support "abstract". Note, we do # support public/private attributes but these are stored # as Access_Spec, not 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.") + type_attr_spec_list = walk(derived_type_stmt, + Fortran2003.Type_Attr_Spec) + + for type_attr_spec in type_attr_spec_list: + # Deal with 'EXTENDS(parent_type)'. + if type_attr_spec.items[0] == "EXTENDS": + extends_name = type_attr_spec.items[1].string + # Look up the extended type in the symbol table + # and specialise the symbol if needed. + extends_symbol = parent.symbol_table.lookup( + extends_name, otherwise=None) + if extends_symbol: + # pylint: disable=unidiomatic-typecheck + if type(extends_symbol) is Symbol: + extends_symbol.specialise(DataTypeSymbol) + extends_symbol.datatype = StructureType() + else: + # If it is not in the symbol table, create a new + # DataTypeSymbol with an UnresolvedInterface for it, + # meaning that we know it exists somewhere but we don't + # know how it is brought into scope, and add it to the + # symbol table. + extends_symbol = DataTypeSymbol( + extends_name, + StructureType(), + interface=UnresolvedInterface()) + parent.symbol_table.add(extends_symbol) + # Set it as the extended type of the new type. + dtype.extends = extends_symbol + else: + raise NotImplementedError("Derived-type definition " + "contains unsupported " + "attributes.") - # We don't yet support derived-type definitions with a CONTAINS - # section. - contains = walk(decl, Fortran2003.Contains_Stmt) - if contains: - raise NotImplementedError( - "Derived-type definition has a CONTAINS statement.") + # We support derived-type definitions with a CONTAINS section. + self._process_derived_type_contains_block(parent, decl, tsymbol) # Re-use the existing code for processing symbols. This needs to # be able to find any symbols declared in an outer scope but @@ -2038,8 +2067,8 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): else: datatype = symbol.datatype initial_value = symbol.initial_value - dtype.add(symbol.name, datatype, symbol.visibility, - initial_value) + dtype.add_component(symbol.name, datatype, + symbol.visibility, initial_value) # Update its type with the definition we've found tsymbol.datatype = dtype @@ -2050,6 +2079,86 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): tsymbol.datatype = UnsupportedFortranType(str(decl)) tsymbol.interface = UnknownInterface() + def _process_derived_type_contains_block(self, parent, decl, tsymbol): + ''' + Process the supplied fparser2 parse tree for a the CONTAINS section of + a derived-type declaration to add any procedures to the DataTypeSymbol. + + :param parent: PSyIR node in which to insert the symbols found. + :type parent: :py:class:`psyclone.psyir.nodes.ScopingNode` + :param decl: fparser2 parse tree of declaration to process. + :type decl: :py:class:`fparser.two.Fortran2003.Type_Declaration_Stmt` + :param tsymbol: the DataTypeSymbol representing the derived-type. + :type tsymbol: :py:class:`psyclone.psyir.symbols.DataTypeSymbol` + + ''' + contains_blocks = walk(decl, Fortran2003.Type_Bound_Procedure_Part) + if not contains_blocks: + return + + # Get it. + contains = contains_blocks[0] + + # Look for any binding-private-stmt (R449) within the + # Specific_Binding part of decln. In the absence of this, the default + # visibility of procedure components is public. + # This only concerns procedure components, i.e. not data ones. + private_stmts = walk(contains, Fortran2003.Binding_Private_Stmt) + if private_stmts: + default_proc_visibility = Symbol.Visibility.PRIVATE + else: + default_proc_visibility = Symbol.Visibility.PUBLIC + + # Get all procedures in the CONTAINS section. + procedures = walk(contains, Fortran2003.Specific_Binding) + + # Process each procedure. + for procedure in procedures: + supported = True + # We do not support interfaces. + if procedure.items[0] is not None: + supported = False + # We do not support 'pass', 'nopass', 'deferred', etc. + if procedure.items[1] is not None: + supported = False + + # Get the name, look it up in the symbol table and + # get its datatype or create it if it does not exist. + procedure_name = procedure.items[3].string + procedure_symbol = parent.symbol_table.lookup(procedure_name, + otherwise=None) + if procedure_symbol and supported: + procedure_datatype = procedure_symbol.datatype + else: + procedure_datatype = UnsupportedFortranType(procedure.string, + None) + + # Get the visibility of the procedure. + procedure_vis = default_proc_visibility + if procedure.items[1] is not None: + access_spec = walk(procedure.items[1], + Fortran2003.Access_Spec) + if access_spec: + procedure_vis = _process_access_spec(access_spec[0]) + + # Deal with the optional initial value. + # This could be, e.g., `null` in `procedure, name => null()` or + # `testkern_code` in `procedure :: code => testkern_code` + if procedure.items[4] is not None: + initial_value_name = procedure.items[4].string + initial_value_symbol = parent.symbol_table.find_or_create( + initial_value_name, allow_renaming=False, + symbol_type=RoutineSymbol, datatype=UnresolvedType()) + initial_value = Reference(initial_value_symbol) + else: + initial_value = None + + # Add this procedure as a component of the derived type + tsymbol.datatype.add_procedure_component(procedure_name, + procedure_datatype, + procedure_vis, + initial_value) + def _get_partial_datatype(self, node, scope, visibility_map): '''Try to obtain partial datatype information from node by removing any unsupported properties in the declaration. @@ -3303,14 +3412,31 @@ def _add_target_attribute(var_name, table): f"be resolved and a DataSymbol") datatype = symbol.datatype - # Create Fortran text for the supplied datatype from the - # supplied UnsupportedFortranType text, then parse this into an - # fparser2 tree and store the fparser2 representation of the - # datatype in type_decl_stmt. - dummy_code = ( - f"subroutine dummy()\n" - f" {datatype.declaration}\n" - f"end subroutine\n") + if isinstance(datatype, UnsupportedFortranType): + # If this is of UnsupportedFortranType, + # create Fortran text for the supplied datatype from the + # supplied UnsupportedFortranType text, then parse this into an + # fparser2 tree and store the fparser2 representation of the + # datatype in type_decl_stmt. + dummy_code = ( + f"subroutine dummy()\n" + f" {datatype.declaration}\n" + f"end subroutine\n") + else: + # If not, this is a supported derived type so we can use the + # backend to generate the Fortran text for the datatype. + # But we need to turn its datatype into an UnsupportedFortranType + # in order to add the 'TARGET' attribute to the declaration. + + # Import here to avoid circular dependencies. + # pylint: disable-next=import-outside-toplevel + from psyclone.psyir.backend.fortran import FortranWriter + dummy_code = ( + f"subroutine dummy()\n" + f" {FortranWriter().gen_vardecl(symbol)}\n" + f"end subroutine\n") + datatype = UnsupportedFortranType("", datatype) + symbol._datatype = datatype parser = ParserFactory().create(std="f2008") reader = FortranStringReader(dummy_code) fp2_ast = parser(reader) diff --git a/src/psyclone/psyir/symbols/data_type_symbol.py b/src/psyclone/psyir/symbols/data_type_symbol.py index 92df96eb9c..f2cf16f36f 100644 --- a/src/psyclone/psyir/symbols/data_type_symbol.py +++ b/src/psyclone/psyir/symbols/data_type_symbol.py @@ -52,17 +52,22 @@ class DataTypeSymbol(Symbol): :type visibility: :py:class:`psyclone.psyir.symbols.Symbol.Visibility` :param interface: the interface to this symbol. :type interface: :py:class:`psyclone.psyir.symbols.SymbolInterface` + :param bool is_class: whether this symbol is used in a 'class' or 'type' + declaration. ''' def __init__(self, name, datatype, visibility=Symbol.DEFAULT_VISIBILITY, - interface=None): + interface=None, + is_class=False): super(DataTypeSymbol, self).__init__(name, visibility, interface) - # The following attribute has a setter method (with error checking) + # The following attributes have setter methods (with error checking) self._datatype = None self.datatype = datatype + self.is_class = is_class + def copy(self): '''Create and return a copy of this object. Any references to the original will not be affected so the copy will not be referred @@ -74,7 +79,8 @@ def copy(self): ''' return type(self)(self.name, self.datatype, visibility=self.visibility, - interface=self.interface.copy()) + interface=self.interface.copy(), + is_class=self.is_class) def __str__(self): return f"{self.name}: {type(self).__name__}" @@ -108,6 +114,31 @@ def datatype(self, value): f"DataType but got: '{type(value).__name__}'") self._datatype = value + @property + def is_class(self): + ''' + :returns: whether this DataTypeSymbol is a 'class' declaration, i.e. + not a 'type' one. + :rtype: bool + ''' + return self._is_class + + @is_class.setter + def is_class(self, value): + ''' Setter for DataTypeSymbol is_class. + + :param bool value: whether this DataTypeSymbol is a 'class' + declaration, i.e. not a 'type' one. + + :raises TypeError: if value is not a bool. + + ''' + if not isinstance(value, bool): + raise TypeError( + f"The is_class attribute of a DataTypeSymbol must be a bool " + f"but got: '{type(value).__name__}'") + self._is_class = value + def copy_properties(self, symbol_in): '''Replace all properties in this object with the properties from symbol_in, apart from the name (which is immutable) and visibility. @@ -123,6 +154,12 @@ def copy_properties(self, symbol_in): f"found '{type(symbol_in).__name__}'.") super(DataTypeSymbol, self).copy_properties(symbol_in) self._datatype = symbol_in.datatype + self._is_class = symbol_in.is_class + + def _process_arguments(self, visibility=None, interface=None, + is_class=False): + super()._process_arguments(visibility, interface) + self.is_class = is_class # For automatic documentation generation diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index ed9a635413..3ffdfb026c 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -927,12 +927,14 @@ class ComponentType: def __init__(self): self._components = OrderedDict() + self._procedure_components = OrderedDict() + self._extends = None def __str__(self): return "StructureType<>" @staticmethod - def create(components): + def create(components, procedure_components=None, extends=None): ''' Creates a StructureType from the supplied list of properties. @@ -945,6 +947,18 @@ def create(components): :py:class:`psyclone.psyir.symbols.Symbol.Visibility`, Optional[:py:class:`psyclone.psyir.symbols.DataNode`] ]] + :param procedure_components: the name, type, visibility (whether + public or private) and initial value (if any) of each procedure + component. + :type procedure_components: Optional[List[tuple[ + str, + :py:class:`psyclone.psyir.symbols.DataType, + :py:class:`psyclone.psyir.symbols.Symbol.Visibility, + Optional[:py:class:`psyclone.psyir.symbols.DataNode`] + ]]] + :param extends: the type that this new type extends (if any). + :type extends: Optional[ + :py:class:`psyclone.psyir.symbols.DataTypeSymbol`] :returns: the new type object. :rtype: :py:class:`psyclone.psyir.symbols.StructureType` @@ -957,7 +971,18 @@ def create(components): f"Each component must be specified using a 4-tuple of " f"(name, type, visibility, initial_value) but found a " f"tuple with {len(component)} members: {component}") - stype.add(*component) + stype.add_component(*component) + if procedure_components: + for procedure_component in procedure_components: + if len(procedure_component) != 4: + raise TypeError( + f"Each procedure component must be specified using a " + f"4-tuple of (name, type, visibility, initial_value) " + f"but found a tuple with {len(procedure_component)} " + f"members: {procedure_component}") + stype.add_procedure_component(*procedure_component) + if extends: + stype.extends = extends return stype @property @@ -968,7 +993,40 @@ def components(self): ''' return self._components - def add(self, name, datatype, visibility, initial_value): + @property + def procedure_components(self): + ''' + :returns: The procedure components of this type. + :rtype: :py:class:`collections.OrderedDict` + ''' + return self._procedure_components + + @property + def extends(self): + ''' + :returns: the type that this new type extends, or None. + :rtype: Union[:py:class:`psyclone.psyir.symbols.DataTypeSymbol`, + None] + + ''' + return self._extends + + @extends.setter + def extends(self, value): + ''' + Set the type that this new type extends. + + :param value: the type that this new type extends. + :type value: :py:class:`psyclone.psyir.symbols.DataTypeSymbol` + + ''' + if not isinstance(value, DataTypeSymbol): + raise TypeError( + f"The type that a StructureType extends must be a " + f"DataTypeSymbol but got '{type(value).__name__}'.") + self._extends = value + + def add_component(self, name, datatype, visibility, initial_value): ''' Create a component with the supplied attributes and add it to this StructureType. @@ -1020,14 +1078,139 @@ def add(self, name, datatype, visibility, initial_value): self._components[name] = self.ComponentType( name, datatype, visibility, initial_value) - def lookup(self, name): + def lookup_component(self, name): ''' - :returns: the ComponentType tuple describing the named member of this \ + :returns: the ComponentType describing the named member of this StructureType. :rtype: :py:class:`psyclone.psyir.symbols.StructureType.ComponentType` ''' return self._components[name] + def add_procedure_component(self, name, datatype, visibility, + initial_value=None): + ''' + Create a procedure component with the supplied attributes and add it to + this StructureType. + + :param str name: the name of the new procedure component. + :param datatype: the type of the new procedure component. + :type datatype: :py:class:`psyclone.psyir.symbols.DataType` + :param visibility: whether this procedure component is public or + private. + :type visibility: :py:class:`psyclone.psyir.symbols.Symbol.Visibility` + :param initial_value: the initial value of the new procedure component. + :type initial_value: + Optional[:py:class:`psyclone.psyir.nodes.DataNode`] + + :raises TypeError: if any of the supplied values are of the wrong type. + + ''' + # These imports must be placed here to avoid circular dependencies. + # pylint: disable=import-outside-toplevel + from psyclone.psyir.symbols import RoutineSymbol + 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)): + raise TypeError( + f"The initial value of a procedure component of a " + f"StructureType must be None or an instance of 'Reference' " + f"but got '{type(initial_value).__name__}'.") + if isinstance(initial_value, Reference): + if not isinstance(initial_value.symbol, RoutineSymbol): + raise TypeError( + f"The initial value of a procedure component of a " + f"StructureType must be None or a Reference to a " + f"RoutineSymbol but got a Reference to a " + f"'{type(initial_value.symbol).__name__}'.") + + self._procedure_components[name] = self.ComponentType( + name, datatype, visibility, initial_value) + + def lookup_procedure_component(self, name): + ''' + :returns: the ComponentType describing the named procedure + member of this StructureType. + :rtype: :py:class:`psyclone.psyir.symbols.StructureType.ComponentType` + ''' + return self._procedure_components[name] + + def replace_procedure_component_initial_value(self, old_value_name, + new_value): + ''' + Replace the initial values of the procedure components with + "old_value_name" as initial value with the supplied new value. + + :param str old_value_name: the name of the initial value to replace. + :param new_value: the new initial value for the procedure component. + :type new_value: :py:class:`psyclone.psyir.nodes.Reference` + + :raises TypeError: if the name is not a string. + :raises TypeError: if the new value is not a Reference to a + RoutineSymbol. + + ''' + # pylint: disable=import-outside-toplevel + # These imports must be placed here to avoid circular dependencies. + from psyclone.psyir.symbols import RoutineSymbol + from psyclone.psyir.nodes import Reference + + if not isinstance(old_value_name, str): + raise TypeError( + "The name of the procedure component to replace must be a " + f"string but got '{type(old_value_name).__name__}'.") + if not isinstance(new_value, Reference): + raise TypeError( + "The new value for the procedure component must be a " + f"Reference but got '{type(new_value).__name__}'.") + if not isinstance(new_value.symbol, RoutineSymbol): + raise TypeError( + "The new value for the procedure component must be a " + f"Reference to a RoutineSymbol but got a Reference to a " + f"'{type(new_value.symbol).__name__}'.") + + for name, procedure_component in self._procedure_components.items(): + if isinstance(procedure_component.datatype, + UnsupportedFortranType): + # Either the procedure component is of UnsupportedFortranType, + # in which case we replace in its whole string declaration. + if old_value_name.lower() not in procedure_component.datatype\ + .declaration.lower(): + continue + new_declaration = procedure_component.datatype\ + .declaration.replace(old_value_name, new_value.name) + new_datatype = UnsupportedFortranType( + new_declaration, + procedure_component.datatype.partial_datatype) + self._procedure_components[name] = self.ComponentType( + name, + new_datatype, + procedure_component.visibility, + new_value) + + # Or it is enough to replace the initial value. + if (not procedure_component.initial_value + or procedure_component.initial_value.name.lower() + != old_value_name.lower()): + continue + self._procedure_components[name] = self.ComponentType( + name, + procedure_component.datatype, + procedure_component.visibility, + new_value) + def __eq__(self, other): ''' :param Any other: the object to check equality to. @@ -1041,9 +1224,15 @@ def __eq__(self, other): if len(self.components) != len(other.components): return False + if len(self.procedure_components) != len(other.procedure_components): + return False + if self.components != other.components: return False + if self.procedure_components != other.procedure_components: + return False + return True def replace_symbols_using(self, table): 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 8ad1d8a97f..91e8dbdd4d 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 @@ -53,7 +53,7 @@ from psyclone.errors import InternalError from psyclone.parse.utils import ParseError from psyclone.psyir.nodes import Container -from psyclone.psyir.symbols import SymbolTable, REAL_TYPE +from psyclone.psyir.symbols import SymbolTable, REAL_TYPE, StructureType METADATA = ("TYPE, EXTENDS(kernel_type) :: compute_cu\n" " TYPE(go_arg), DIMENSION(4) :: meta_args = (/ &\n" @@ -209,8 +209,8 @@ def test_goceankernelmetadata_create1(fortran_reader): symbol._datatype = REAL_TYPE 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)) + assert ("Expected kernel metadata to be stored in the PSyIR as a " + "StructureType, but found ScalarType." in str(info.value)) # create_from_fortran_string @@ -392,16 +392,21 @@ def test_getproperty_error(): "EXTENDS(kernel_type)" in str(info.value)) -def test_getproperty(fortran_reader): +def test_getproperty(fortran_reader, fortran_writer): '''Test utility function that takes metadata in an fparser2 tree and returns the value associated with the supplied property name. ''' kernel_psyir = fortran_reader.psyir_from_source(PROGRAM) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "compute_cu").datatype + datatype_symbol = kernel_psyir.children[0].symbol_table.lookup( + "compute_cu") + datatype = datatype_symbol.datatype metadata = GOceanKernelMetadata() - reader = FortranStringReader(datatype.declaration) + + assert isinstance(datatype, StructureType) + type_declaration = fortran_writer.gen_typedecl(datatype_symbol) + reader = FortranStringReader(type_declaration) + spec_part = Fortran2003.Derived_Type_Def(reader) assert metadata._get_property(spec_part, "code").string == \ "compute_cu_code" @@ -409,8 +414,8 @@ def test_getproperty(fortran_reader): "GO_ALL_PTS" with pytest.raises(ParseError) as info: metadata._get_property(spec_part, "not_found") - assert ("'not_found' was not found in TYPE, EXTENDS(kernel_type) :: " - "compute_cu" in str(info.value)) + assert ("'not_found' was not found in TYPE, EXTENDS(kernel_type), PUBLIC " + ":: compute_cu" in str(info.value)) def test_iteratesover(): 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 cfa2493f96..602006e4da 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 @@ -50,7 +50,7 @@ from psyclone.errors import InternalError from psyclone.parse.utils import ParseError from psyclone.psyir.symbols import DataTypeSymbol, REAL_TYPE, \ - UnsupportedFortranType + UnsupportedFortranType, StructureType # pylint: disable=too-many-statements @@ -1134,27 +1134,19 @@ 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(fortran_reader, fortran_writer): '''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) - 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 + datatype_symbol = kernel_psyir.children[0].symbol_table.lookup( + "testkern_type") + datatype = datatype_symbol.datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) + assert isinstance(datatype, StructureType) + type_declaration = fortran_writer.gen_typedecl(datatype_symbol) + reader = FortranStringReader(type_declaration) spec_part = Fortran2003.Derived_Type_Def(reader) binding = spec_part.children[2] binding.children[1] = binding.children[0] @@ -1165,10 +1157,13 @@ def test_get_procedure_name_error(fortran_reader): kernel_psyir = fortran_reader.psyir_from_source(PROGRAM.replace( "code", "hode")) - datatype = kernel_psyir.children[0].symbol_table.lookup( - "testkern_type").datatype + datatype_symbol = kernel_psyir.children[0].symbol_table.lookup( + "testkern_type") + datatype = datatype_symbol.datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) + assert isinstance(datatype, StructureType) + type_declaration = fortran_writer.gen_typedecl(datatype_symbol) + reader = FortranStringReader(type_declaration) spec_part = Fortran2003.Derived_Type_Def(reader) with pytest.raises(ParseError) as info: metadata._get_procedure_name(spec_part) @@ -1177,17 +1172,20 @@ def test_get_procedure_name_error(fortran_reader): in str(info.value)) -def test_get_procedure_name(fortran_reader): +def test_get_procedure_name(fortran_reader, fortran_writer): '''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 + datatype_symbol = kernel_psyir.children[0].symbol_table.lookup( + "testkern_type") + datatype = datatype_symbol.datatype metadata = LFRicKernelMetadata() - reader = FortranStringReader(datatype.declaration) + assert isinstance(datatype, StructureType) + type_declaration = fortran_writer.gen_typedecl(datatype_symbol) + reader = FortranStringReader(type_declaration) spec_part = Fortran2003.Derived_Type_Def(reader) assert metadata._get_procedure_name(spec_part) == \ "testkern_code" diff --git a/src/psyclone/tests/psyGen_test.py b/src/psyclone/tests/psyGen_test.py index c5277faea7..4cb9206ac0 100644 --- a/src/psyclone/tests/psyGen_test.py +++ b/src/psyclone/tests/psyGen_test.py @@ -66,10 +66,11 @@ GlobalSum, InvokeSchedule, BuiltIn) from psyclone.psyir.nodes import (Assignment, BinaryOperation, Container, Literal, Loop, Node, KernelSchedule, Call, - colored, Schedule) + colored, Schedule, Reference) from psyclone.psyir.symbols import (DataSymbol, RoutineSymbol, REAL_TYPE, ImportInterface, ContainerSymbol, Symbol, - INTEGER_TYPE, UnresolvedType, SymbolTable) + INTEGER_TYPE, UnresolvedType, SymbolTable, + StructureType, UnsupportedFortranType) from psyclone.tests.lfric_build import LFRicBuild from psyclone.tests.test_files import dummy_transformations from psyclone.tests.test_files.dummy_transformations import LocalTransformation @@ -663,6 +664,168 @@ def test_codedkern_lower_to_language_level(monkeypatch): assert csymbol.name == "testkern_mod" +def test_codedkern__rename_psyir_unsupported_procedure_datatype(): + ''' Check that the CodedKern rename method renames the kernel in the PSyIR + tree. ''' + # pylint: disable=protected-access, too-many-statements + psy, _ = get_invoke("1_single_invoke.f90", "lfric", + idx=0, dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + kern = schedule.children[0].loop_body[0] + assert isinstance(kern, CodedKern) + + # Ensure everything is as expected before renaming + kern_schedule = kern.get_kernel_schedule() + assert kern_schedule.name == "testkern_code" + + container = kern_schedule.ancestor(Container) + assert isinstance(container, Container) + assert container.name == "testkern_mod" + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + assert isinstance(testkern_type.datatype, StructureType) + assert len(testkern_type.datatype.procedure_components) == 1 + assert "code" in testkern_type.datatype.procedure_components + assert testkern_type.datatype.procedure_components["code"].name == "code" + assert isinstance(testkern_type.datatype.procedure_components["code"] + .datatype, + UnsupportedFortranType) + + # Rename the kernel in the PSyIR tree + kern._rename_psyir(suffix="_new") + kern_schedule = kern.get_kernel_schedule() + assert kern_schedule.name == "testkern_new_code" + container = kern_schedule.ancestor(Container) + assert container.name == "testkern_new_mod" + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + assert isinstance(testkern_type.datatype.procedure_components["code"] + .datatype, + UnsupportedFortranType) + # In this case string substitution is applied in the .declaration string + assert ("code => testkern_new_code" + in testkern_type.datatype.procedure_components["code"].datatype + .declaration) + + +def test_codedkern__rename_psyir_supported_procedure_datatype(): + ''' Check that the CodedKern rename method renames the kernel in the PSyIR + tree. ''' + # pylint: disable=protected-access, too-many-statements + psy, _ = get_invoke("1_single_invoke.f90", "lfric", + idx=0, dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + kern = schedule.children[0].loop_body[0] + assert isinstance(kern, CodedKern) + + # Ensure everything is as expected before renaming + kern_schedule = kern.get_kernel_schedule() + assert kern_schedule.name == "testkern_code" + + container = kern_schedule.ancestor(Container) + assert isinstance(container, Container) + assert container.name == "testkern_mod" + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + assert isinstance(testkern_type.datatype, StructureType) + assert len(testkern_type.datatype.procedure_components) == 1 + assert "code" in testkern_type.datatype.procedure_components + assert testkern_type.datatype.procedure_components["code"].name == "code" + assert isinstance(testkern_type.datatype.procedure_components["code"] + .datatype, + UnsupportedFortranType) + + # Make the procedure component of the structure type be of a supported type + psy, _ = get_invoke("1_single_invoke.f90", "lfric", + idx=0, dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + kern = schedule.children[0].loop_body[0] + assert isinstance(kern, CodedKern) + kern_schedule = kern.get_kernel_schedule() + container = kern_schedule.ancestor(Container) + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + procedure_component = testkern_type.datatype.procedure_components["code"] + assert isinstance(procedure_component.datatype, UnsupportedFortranType) + # Replace the procedure component + routine_symbol = container_table.lookup("testkern_code") + new_procedure_component = StructureType.ComponentType( + procedure_component.name, + UnresolvedType(), + procedure_component.visibility, + # This initial_value part should be renamed + Reference(routine_symbol)) + testkern_type.datatype.procedure_components["code"] \ + = new_procedure_component + # Check that the procedure component has been replaced + assert (testkern_type.datatype.procedure_components["code"] + .initial_value is not None) + assert (testkern_type.datatype.procedure_components["code"] + .initial_value.name.lower() == "testkern_code") + # Rename + kern._rename_psyir(suffix="_new2") + assert (testkern_type.datatype.procedure_components["code"] + .name == procedure_component.name) + assert isinstance(testkern_type.datatype.procedure_components["code"] + .datatype, UnresolvedType) + assert (testkern_type.datatype.procedure_components["code"] + .visibility == procedure_component.visibility) + assert isinstance(testkern_type.datatype.procedure_components["code"] + .initial_value, Reference) + assert (testkern_type.datatype.procedure_components["code"] + .initial_value.symbol + == container_table.lookup("testkern_new2_code")) + assert (testkern_type.datatype.procedure_components["code"] + .initial_value.name == "testkern_new2_code") + + +def test_codedkern__rename_psyir_unsupported_datatypesymbol_datatype(): + ''' Check that the CodedKern rename method renames the kernel in the PSyIR + tree. ''' + # pylint: disable=protected-access, too-many-statements + psy, _ = get_invoke("1_single_invoke.f90", "lfric", + idx=0, dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + kern = schedule.children[0].loop_body[0] + assert isinstance(kern, CodedKern) + + # Ensure everything is as expected before renaming + kern_schedule = kern.get_kernel_schedule() + assert kern_schedule.name == "testkern_code" + + container = kern_schedule.ancestor(Container) + assert isinstance(container, Container) + assert container.name == "testkern_mod" + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + assert isinstance(testkern_type.datatype, StructureType) + assert len(testkern_type.datatype.procedure_components) == 1 + assert "code" in testkern_type.datatype.procedure_components + assert testkern_type.datatype.procedure_components["code"].name == "code" + assert isinstance(testkern_type.datatype.procedure_components["code"] + .datatype, + UnsupportedFortranType) + + # Make the DataTypeSymbol be of UnsupportedFortranType + psy, _ = get_invoke("1_single_invoke.f90", "lfric", + idx=0, dist_mem=False) + schedule = psy.invokes.invoke_list[0].schedule + kern = schedule.children[0].loop_body[0] + assert isinstance(kern, CodedKern) + container = kern.get_kernel_schedule().ancestor(Container) + container_table = container.symbol_table + testkern_type = container_table.lookup("testkern_type") + assert isinstance(testkern_type.datatype, StructureType) + # This is only to test string substitution so use a fake type string + testkern_type._datatype = UnsupportedFortranType( + "type, extends(kernel_type) :: testkern_type [...] code " + "=> testkern_code [...]") + kern._rename_psyir(suffix="_new") + assert (testkern_type.datatype.declaration == + "type, extends(kernel_type) :: testkern_type [...] code => " + "testkern_new_code [...]") + + def test_kern_coloured_text(): '''Check that the coloured_name method of both CodedKern and BuiltIn return what we expect. 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 e2b713de0d..2183309139 100644 --- a/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py +++ b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint.py @@ -51,7 +51,7 @@ 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) + DataSymbol, ArgumentInterface, INTEGER_TYPE, REAL_TYPE, StructureType) def test_generate_lfric_adjoint_no_container_error(fortran_reader): @@ -275,11 +275,9 @@ def test_generate_lfric_adjoint_multi_precision( sym_table = psyir.children[0].symbol_table test_type_symbol = sym_table.lookup("test_type") datatype = test_type_symbol.datatype + assert isinstance(datatype, StructureType) # Remove procedure metadata - new_declaration = (datatype.declaration. - replace("PROCEDURE, NOPASS :: kern_code", ""). - replace("CONTAINS", "")) - datatype._declaration = new_declaration + 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/psyad/domain/lfric/test_lfric_adjoint_harness.py b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint_harness.py index 6361797a09..90e986ecfb 100644 --- a/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint_harness.py +++ b/src/psyclone/tests/psyad/domain/lfric/test_lfric_adjoint_harness.py @@ -546,6 +546,7 @@ def test_generate_lfric_adjoint_harness_invalid_code(fortran_reader): " use kinds_mod, only: i_def, r_def\n" " use kernel_mod, only: kernel_type, arg_type, gh_field, gh_real, " "gh_write, w3, cell_column\n" + " use argument_mod, only: func_type, gh_quadrature_xyoz\n" " type, extends(kernel_type) :: testkern_type\n" " type(arg_type), dimension(2) :: meta_args = & \n" " (/ arg_type(gh_scalar, gh_real, gh_read), & \n" diff --git a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py index 3961dd42ba..0d597b8a32 100644 --- a/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_gen_decls_test.py @@ -47,7 +47,7 @@ RoutineSymbol, ScalarType, Symbol, SymbolTable, UnresolvedType, StructureType, ImportInterface, UnresolvedInterface, ArgumentInterface, INTEGER_TYPE, REAL_TYPE, StaticInterface, PreprocessorInterface, - CHARACTER_TYPE) + CHARACTER_TYPE, UnsupportedFortranType) def test_gen_param_decls_dependencies(fortran_writer): @@ -364,3 +364,71 @@ def test_gen_interfacedecl(fortran_writer): procedure :: sub1 end interface subx ''') + + +def test_fw_gen_proceduredecl(fortran_writer): + '''Test the FortranWriter class gen_proceduredecl method produces the + expected declarations and raises the expected exceptions. + ''' + with pytest.raises(VisitorError) as err: + fortran_writer.gen_proceduredecl(None) + assert ("gen_proceduredecl() expects a 'DataSymbol' or " + "'StructureType.ComponentType' as its first " + "argument but got 'NoneType'" in str(err.value)) + + # A DataSymbol of UnupportedFortranType + symbol = DataSymbol("my_sub", UnsupportedFortranType( + "procedure, private :: my_unsupported_procedure")) + assert (fortran_writer.gen_proceduredecl(symbol) == + "procedure, private :: my_unsupported_procedure\n") + + # A StructureType.ComponentType with 'public' visibility and no initial + # value + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PUBLIC, None) + assert (fortran_writer.gen_proceduredecl(dtype) == + "procedure, public :: my_procedure\n") + + # A StructureType.ComponentType with 'public' visibility and an initial + # value + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PUBLIC, + Reference(RoutineSymbol("other", + REAL_TYPE))) + assert (fortran_writer.gen_proceduredecl(dtype) == + "procedure, public :: my_procedure => other\n") + + # A StructureType.ComponentType with 'private' visibility and no initial + # value + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PRIVATE, None) + assert fortran_writer.gen_proceduredecl(dtype) == ( + "procedure, private :: my_procedure\n") + + # A StructureType.ComponentType with 'private' visibility and an initial + # value + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PRIVATE, + Reference(RoutineSymbol("other", + REAL_TYPE))) + assert fortran_writer.gen_proceduredecl(dtype) == ( + "procedure, private :: my_procedure => other\n") + + # Check that visibility is not included in the output if include_visibility + # is False + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PUBLIC, None) + assert (fortran_writer.gen_proceduredecl(dtype, include_visibility=False) + == ("procedure :: my_procedure\n")) + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + Symbol.Visibility.PRIVATE, None) + assert (fortran_writer.gen_proceduredecl(dtype, include_visibility=False) + == ("procedure :: my_procedure\n")) + + # Check exception is raised if visibility is not 'public' or 'private' + dtype = StructureType.ComponentType("my_procedure", REAL_TYPE, + "wrong", None) + with pytest.raises(InternalError) as err: + fortran_writer.gen_proceduredecl(dtype) + assert ("A Symbol must be either public or private but symbol " + "'my_procedure' has visibility 'wrong'" in str(err.value)) diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py index d51fbfd52b..d4214a6f5e 100644 --- a/src/psyclone/tests/psyir/backend/fortran_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_test.py @@ -373,8 +373,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 ("gen_typedecl expects a DataTypeSymbol with a StructureType " + "as its datatype but got: 'UnresolvedType'" in str(err.value)) def test_gen_typedecl_unsupported_fortran_type(fortran_writer): 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 2ad3382fb5..e4e921327b 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_derived_type_test.py @@ -157,9 +157,9 @@ def test_name_clash_derived_type(f2008_parser, type_name): # already contain a RoutineSymbol named 'my_type' with pytest.raises(SymbolError) as err: processor.process_declarations(fake_parent, spec_part.children, []) - assert (f"Search for a DataTypeSymbol named '{type_name}' (required by " - f"specification 'TYPE({type_name})') found a 'RoutineSymbol' " - f"instead" in str(err.value)) + assert (f"Search for a DataTypeSymbol named '{type_name.lower()}' " + f"(required by specification 'TYPE({type_name})') found a " + f"'RoutineSymbol' instead" in str(err.value)) def test_name_clash_derived_type_def(f2008_parser): @@ -256,14 +256,14 @@ def test_parse_derived_type(use_stmt, type_name): sym = symtab.lookup("my_type") assert isinstance(sym, DataTypeSymbol) assert isinstance(sym.datatype, StructureType) - flag = sym.datatype.lookup("flag") + flag = sym.datatype.lookup_component("flag") assert isinstance(flag.datatype, ScalarType) assert flag.visibility == Symbol.Visibility.PUBLIC - grid = sym.datatype.lookup("grid") + grid = sym.datatype.lookup_component("grid") assert isinstance(grid.datatype, DataTypeSymbol) assert isinstance(grid.datatype.datatype, UnresolvedType) assert grid.visibility == Symbol.Visibility.PRIVATE - posn = sym.datatype.lookup("posn") + posn = sym.datatype.lookup_component("posn") assert isinstance(posn.datatype, ArrayType) var = symtab.lookup("var") assert var.datatype is sym @@ -271,7 +271,7 @@ 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 + ''' Check that we get a DataTypeSymbol of StructureType if a derived-type definition has a CONTAINS section. ''' fake_parent = KernelSchedule.create("dummy_schedule") symtab = fake_parent.symbol_table @@ -286,16 +286,11 @@ 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. + # It should still be a DataTypeSymbol and its type is known. 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 len(sym.datatype.procedure_components) == 1 @pytest.mark.usefixtures("f2008_parser") @@ -339,9 +334,10 @@ def test_derived_type_accessibility(): processor.process_declarations(fake_parent, fparser2spec.content, []) sym = symtab.lookup("my_type") assert isinstance(sym, DataTypeSymbol) - flag = sym.datatype.lookup("flag") + assert isinstance(sym.datatype, StructureType) + flag = sym.datatype.components["flag"] assert flag.visibility == Symbol.Visibility.PRIVATE - scale = sym.datatype.lookup("scale") + scale = sym.datatype.components["scale"] assert scale.visibility == Symbol.Visibility.PUBLIC diff --git a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py index 4d494bdee4..c1300ac50d 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_select_type_test.py @@ -72,7 +72,7 @@ def test_type(fortran_reader, fortran_writer, tmpdir): " END SELECT\n" "end subroutine\n" "end module\n") - expected1 = "CLASS(*), TARGET :: type_selector" + expected1 = "class(*), target :: type_selector" expected2 = ( " character(256) :: type_string\n" " INTEGER, pointer :: ptr_INTEGER => null()\n" @@ -377,7 +377,7 @@ def test_kind(fortran_reader, fortran_writer, tmpdir): "end subroutine\n" "end module\n") expected1 = ( - " CLASS(*), TARGET :: type\n" + " class(*), target :: type\n" " integer :: branch1\n" " integer :: branch2\n" " REAL(KIND = 4) :: rinfo1\n" @@ -435,7 +435,7 @@ def test_derived(fortran_reader, fortran_writer, tmpdir): "end subroutine\n" "end module\n") expected1 = ( - " CLASS(*), TARGET :: type\n" + " class(*), target :: type\n" " type :: field_type\n" " integer :: x\n" " end type field_type\n" @@ -455,9 +455,9 @@ def test_derived(fortran_reader, fortran_writer, tmpdir): " field_type_info = ptr_field_type\n" " end if\n") psyir = fortran_reader.psyir_from_source(code) - result = fortran_writer(psyir) - assert expected1 in result - assert expected2 in result + result = fortran_writer(psyir).lower() + assert expected1.lower() in result + assert expected2.lower() in result assert Compile(tmpdir).string_compiles(result) @@ -492,7 +492,7 @@ def test_datatype(fortran_reader, fortran_writer, tmpdir): "end subroutine\n" "end module\n") expected1 = ( - " CLASS(*), TARGET :: type_selector\n" + " class(*), target :: type_selector\n" " integer :: branch1\n" " integer :: branch2\n" " integer :: branch3\n" @@ -564,7 +564,7 @@ def test_character(fortran_reader, fortran_writer, tmpdir, char_type_in, f"end module\n") expected1 = ( f" subroutine select_type(type_selector)\n" - f" CLASS(*), TARGET :: type_selector\n" + f" class(*), target :: type_selector\n" f" CHARACTER{char_type_out} :: character_type\n" f" character(256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => " @@ -609,7 +609,7 @@ def test_character_assumed_len(fortran_reader, fortran_writer, tmpdir, f"end module\n") expected1 = ( f" subroutine select_type(type_selector)\n" - f" CLASS(*), TARGET :: type_selector\n" + f" class(*), target :: type_selector\n" f" CHARACTER{char_type_out}, POINTER :: character_type => null()\n" f" character(256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => " @@ -723,7 +723,7 @@ def test_class_target( f" public\n\n" f" contains\n" f" subroutine select_type(type_selector, character_type)\n" - f" CLASS(*), {post_attribute} :: type_selector\n" + f" class(*), {post_attribute.lower()} :: type_selector\n" f" CHARACTER(LEN = *) :: character_type\n" f" character(256) :: type_string\n" f" CHARACTER(LEN=256), pointer :: ptr_CHARACTER_star => null()\n\n" diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index b7fbb66eab..925dd659f4 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -614,6 +614,46 @@ def test_process_declarations(): assert isinstance(ptr_sym.initial_value, IntrinsicCall) +@pytest.mark.usefixtures("f2008_parser") +def test_process_declarations_class_keyword(): + '''Test that process_declarations method of Fparser2Reader + converts the fparser2 declarations with the class keyword to symbols + in the provided parent Kernel Schedule. + + ''' + fake_parent = KernelSchedule.create("dummy_schedule") + processor = Fparser2Reader() + + # Class declaration + reader = FortranStringReader("class(my_type), intent(in) :: carg") + # Set reader to free format (otherwise this is a comment in fixed format) + reader.set_format(FortranFormat(True, True)) + fparser2spec = Specification_Part(reader).content[0] + processor.process_declarations(fake_parent, [fparser2spec], []) + sym = fake_parent.symbol_table.lookup("carg") + assert isinstance(sym, DataSymbol) + assert isinstance(sym.datatype, DataTypeSymbol) + assert sym.datatype.is_class is True + assert sym.name == "carg" + assert sym.datatype.name == "my_type" + assert isinstance(sym.interface, ArgumentInterface) + assert sym.interface.access == ArgumentInterface.Access.READ + + # class(*) case + reader = FortranStringReader("class(*), intent(in) :: urgh") + reader.set_format(FortranFormat(True, True)) + fparser2spec = Specification_Part(reader).content[0] + processor.process_declarations(fake_parent, [fparser2spec], []) + sym = fake_parent.symbol_table.lookup("urgh") + assert isinstance(sym, DataSymbol) + assert isinstance(sym.datatype, DataTypeSymbol) + assert sym.datatype.is_class is True + assert sym.name == "urgh" + assert sym.datatype.name == "*" + assert isinstance(sym.interface, ArgumentInterface) + assert sym.interface.access == ArgumentInterface.Access.READ + + @pytest.mark.usefixtures("f2008_parser") @pytest.mark.parametrize("decln_text", ["integer, pointer :: l1 => null(), l2 => null()", @@ -1205,16 +1245,6 @@ def test_process_not_supported_declarations(): assert isinstance(fake_parent.symbol_table.lookup("p3").datatype, UnsupportedFortranType) - reader = FortranStringReader("class(my_type), intent(in) :: carg") - # Set reader to free format (otherwise this is a comment in fixed format) - reader.set_format(FortranFormat(True, True)) - fparser2spec = Specification_Part(reader).content[0] - processor.process_declarations(fake_parent, [fparser2spec], []) - sym = fake_parent.symbol_table.lookup("carg") - assert isinstance(sym.datatype, UnsupportedFortranType) - assert (sym.datatype.declaration.lower() == - "class(my_type), intent(in) :: carg") - # Allocatable but with specified extent. This is invalid Fortran but # fparser2 doesn't spot it (see fparser/#229). reader = FortranStringReader("integer, allocatable :: l10(5)") @@ -2631,14 +2661,11 @@ def raise_value_error(_1, _2): assert "error to propagate" in str(err.value) -def test_structures(fortran_reader, fortran_writer): +def test_structures_component_init(fortran_reader, + fortran_writer): '''Test that Fparser2Reader parses Fortran types correctly when there - is a type declaration with one of the members being initialised, - when there is a type declaration with an additional attribute - (e.g. one that extends an existing type), when there is a type - declaration that contains procedures and when there is a type - declaration that both has an additional attribute and contains - procedures. + is a type declaration with one of the members being initialised, with + visibility of the derived type either inline or separate. ''' # derived-type with initial value (StructureType) and in-line visibility @@ -2682,7 +2709,15 @@ def test_structures(fortran_reader, fortran_writer): " integer, public :: j\n" " end type my_type\n" in result) - # type that extends another type (UnsupportedFortranType) + +def test_structures_extends(fortran_reader, fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with one of the members being initialised, + and a type declaration with an additional extends attribute, either + know or unknown.''' + + # type that extends another type, known in the symbol table + # (StructureType) test_code = ( "module test_mod\n" " use kernel_mod, only : kernel_type\n" @@ -2694,14 +2729,45 @@ 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 isinstance(symbol.datatype.extends, DataTypeSymbol) + assert symbol.datatype.extends.name == "kernel_type" 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 extends another type, unknown (StructureType) + test_code = ( + "module test_mod\n" + " use kernel_mod\n" + " type, extends(kernel_type) :: my_type\n" + " integer :: i = 1\n" + " end type my_type\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("my_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert isinstance(symbol.datatype.extends, DataTypeSymbol) + assert symbol.datatype.extends.name == "kernel_type" + result = fortran_writer(psyir) + assert ( + " type, extends(kernel_type), public :: my_type\n" + " integer, public :: i = 1\n" + " end type my_type\n" in result) + # Check that kernel_type is not declared in the module + assert ":: kernel_type" not in result + + +def test_structures_with_procedure(fortran_reader, fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with one of the members being initialised, + and that contains procedures.''' + + # type that contains a procedure (StructureType) test_code = ( "module test_mod\n" " type :: test_type\n" @@ -2717,14 +2783,36 @@ 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) result = fortran_writer(psyir) assert ( " type, public :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure, nopass :: test_code\n" + " end type test_type\n" in result) + + +def test_structures_abstract(fortran_reader, fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with an abstract keyword.''' + # abstract type (UnsupportedFortranType) + test_code = ( + "module test_mod\n" + " type, abstract :: my_type\n" + " integer :: i = 1\n" + " end type my_type\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("my_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, UnsupportedFortranType) + result = fortran_writer(psyir) + assert ( + " type, abstract, public :: my_type\n" " INTEGER :: i = 1\n" - " CONTAINS\n" - " PROCEDURE, NOPASS :: test_code\n" - "END TYPE test_type\n" in result) + "END TYPE my_type\n" in result) # type that creates an abstract type and contains a procedure # (UnsupportedFortranType) @@ -2753,6 +2841,247 @@ def test_structures(fortran_reader, fortran_writer): "END TYPE test_type\n" in result) +def test_structures_type_with_inline_procedures_visibility(fortran_reader, + fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with inline visibilities on procedure components.''' + # public type that contains procedures + test_code = ( + "module test_mod\n" + " use kernel_mod, only : parent_type\n" + " type, extends(parent_type) :: test_type\n" + " integer :: i = 1\n" + " contains\n" + " procedure :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + " subroutine test_code_private()\n" + " end subroutine\n" + " subroutine test_code_public()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 3 + result = fortran_writer(psyir) + assert ( + " type, extends(parent_type), public :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure, public :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" in result) + + # private type that contains procedures + # the `test_code` procedure and `i` component should stay public + test_code = ( + "module test_mod\n" + " use kernel_mod, only : parent_type\n" + " type, extends(parent_type), private :: test_type\n" + " integer :: i = 1\n" + " contains\n" + " procedure :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + " subroutine test_code_private()\n" + " end subroutine\n" + " subroutine test_code_public()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 3 + result = fortran_writer(psyir) + assert ( + " type, extends(parent_type), private :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure, public :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" in result) + + +def test_structures_type_with_standalone_procedures_visibility(fortran_reader, + fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with standalone visibilities on procedure or data + components.''' + # type that contains procedures and a private statement in the contains + # section + # the `test_code` procedure should thus become private + # but `i` should stay public + test_code = ( + "module test_mod\n" + " use kernel_mod, only : parent_type\n" + " type, extends(parent_type) :: test_type\n" + " integer :: i = 1\n" + " contains\n" + " private\n" + " procedure :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + " subroutine test_code_private()\n" + " end subroutine\n" + " subroutine test_code_public()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 3 + result = fortran_writer(psyir) + assert ( + " type, extends(parent_type), public :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure, private :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" in result) + + # type that contains procedures and a private statement outside of the + # contains section + # the `i` component should become private + # but the `test_code` procedure should stay public + test_code = ( + "module test_mod\n" + " use kernel_mod, only : parent_type\n" + " type, extends(parent_type) :: test_type\n" + " private\n" + " integer :: i = 1\n" + " contains\n" + " procedure :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + " subroutine test_code_private()\n" + " end subroutine\n" + " subroutine test_code_public()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 3 + result = fortran_writer(psyir) + assert ( + " type, extends(parent_type), public :: test_type\n" + " integer, private :: i = 1\n" + " contains\n" + " procedure, public :: test_code\n" + " procedure, private :: test_code_private\n" + " procedure, public :: test_code_public\n" + " end type test_type\n" in result) + + +def test_structures_type_with_procedure_interface(fortran_reader, + fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with a procedure component having an interface.''' + # type that contains a procedure with an interface name + test_code = ( + "module test_mod\n" + " type :: test_type\n" + " integer :: i = 1\n" + " contains\n" + " procedure (my_interface) :: test_code\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 1 + assert "test_code" in symbol.datatype.procedure_components + assert isinstance(symbol.datatype.procedure_components["test_code"]. + datatype, UnsupportedFortranType) + result = fortran_writer(psyir) + assert ( + " type, public :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure (my_interface) :: test_code\n" + " end type test_type\n" in result) + + +def test_structures_type_with_procedure_unsupported_attributes(fortran_reader, + fortran_writer): + '''Test that Fparser2Reader parses Fortran types correctly when there + is a type declaration with procedure components having unsupported + attributes.''' + # type that contains procedures with pass, nopass and deferred + test_code = ( + "module test_mod\n" + " type :: test_type\n" + " integer :: i = 1\n" + " contains\n" + " procedure, pass :: test_code_pass\n" + " procedure, nopass :: test_code_nopass\n" + " procedure, deferred :: test_code_deferred\n" + " end type test_type\n" + " contains\n" + " subroutine test_code()\n" + " end subroutine\n" + "end module test_mod\n") + psyir = fortran_reader.psyir_from_source(test_code) + sym_table = psyir.children[0].symbol_table + symbol = sym_table.lookup("test_type") + assert isinstance(symbol, DataTypeSymbol) + assert isinstance(symbol.datatype, StructureType) + assert len(symbol.datatype.procedure_components) == 3 + assert "test_code_pass" in symbol.datatype.procedure_components + assert isinstance(symbol.datatype.procedure_components["test_code_pass"]. + datatype, UnsupportedFortranType) + assert "test_code_nopass" in symbol.datatype.procedure_components + assert isinstance(symbol.datatype. + procedure_components["test_code_nopass"]. + datatype, UnsupportedFortranType) + assert "test_code_deferred" in symbol.datatype.procedure_components + assert isinstance(symbol.datatype. + procedure_components["test_code_deferred"]. + datatype, UnsupportedFortranType) + result = fortran_writer(psyir) + assert ( + " type, public :: test_type\n" + " integer, public :: i = 1\n" + " contains\n" + " procedure, pass :: test_code_pass\n" + " procedure, nopass :: test_code_nopass\n" + " procedure, deferred :: test_code_deferred\n" + " end type test_type\n" in result) + + def test_structures_constants(fortran_reader, fortran_writer): '''Test that Fparser2Reader parses Fortran types correctly when there is a type declaration with one of the members being initialised @@ -2776,7 +3105,7 @@ def test_structures_constants(fortran_reader, fortran_writer): symbol = sym_table.lookup("my_type") assert isinstance(symbol, DataTypeSymbol) assert isinstance(symbol.datatype, StructureType) - i_symbol = symbol.datatype.lookup("i") + i_symbol = symbol.datatype.lookup_component("i") n_reference = i_symbol.initial_value.children[0] assert n_reference.symbol is n_symbol m_reference = i_symbol.initial_value.children[1] @@ -2818,7 +3147,7 @@ def test_structures_constant_scope(fortran_reader, fortran_writer): symbol = sym_table.lookup("my_type") assert isinstance(symbol, DataTypeSymbol) assert isinstance(symbol.datatype, StructureType) - i_symbol = symbol.datatype.lookup("i") + i_symbol = symbol.datatype.lookup_component("i") n_reference = i_symbol.initial_value.children[0] assert n_reference.symbol is n_symbol m_reference = i_symbol.initial_value.children[1] @@ -2868,7 +3197,7 @@ def test_structures_constant_use(fortran_reader, fortran_writer): symbol = sym_table.lookup("my_type") assert isinstance(symbol, DataTypeSymbol) assert isinstance(symbol.datatype, StructureType) - i_symbol = symbol.datatype.lookup("i") + i_symbol = symbol.datatype.lookup_component("i") n_reference = i_symbol.initial_value.children[0] assert n_reference.symbol is n_symbol m_reference = i_symbol.initial_value.children[1] diff --git a/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py b/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py index 11dc71ea85..2f1d49293c 100644 --- a/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py +++ b/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py @@ -62,6 +62,16 @@ def test_create_datatypesymbol_wrong_datatype(): "DataType but got: 'str'" in str(err.value)) +def test_create_datatypesymbol_wrong_is_class(): + ''' Check that attempting to specify the is_class attribute of a + DataTypeSymbol with an invalid type results in the expected error. ''' + sym = DataTypeSymbol("my_type", UnresolvedType()) + with pytest.raises(TypeError) as err: + sym.is_class = "integer" + assert ("The is_class attribute of a DataTypeSymbol must be a bool " + "but got: 'str'" in str(err.value)) + + def test_datatypesymbol_copy(): ''' Check that a DataTypeSymbol can be copied. ''' symbol = DataTypeSymbol("my_type", UnresolvedType(), diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index 32b9ebe76b..56cb469ff9 100644 --- a/src/psyclone/tests/psyir/symbols/datatype_test.py +++ b/src/psyclone/tests/psyir/symbols/datatype_test.py @@ -46,7 +46,7 @@ from psyclone.psyir.symbols import ( ArrayType, DataType, UnresolvedType, ScalarType, UnsupportedFortranType, DataSymbol, StructureType, NoType, INTEGER_TYPE, REAL_TYPE, Symbol, - DataTypeSymbol, SymbolTable) + DataTypeSymbol, SymbolTable, RoutineSymbol) # Abstract DataType class @@ -890,44 +890,81 @@ def test_unsupported_fortran_type_replace_symbols(): # StructureType tests def test_structure_type(): - ''' Check the StructureType constructor and that we can add components. ''' + ''' Check the StructureType constructor and that we can add components + and procedure_components. ''' stype = StructureType() assert str(stype) == "StructureType<>" assert not stype.components - stype.add("flag", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None) - flag = stype.lookup("flag") + stype.add_component("flag", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None) + flag = stype.lookup_component("flag") assert not flag.initial_value assert isinstance(flag, StructureType.ComponentType) - stype.add("flag2", INTEGER_TYPE, Symbol.Visibility.PUBLIC, - Literal("1", INTEGER_TYPE)) - flag2 = stype.lookup("flag2") + stype.add_component("flag2", INTEGER_TYPE, Symbol.Visibility.PUBLIC, + Literal("1", INTEGER_TYPE)) + flag2 = stype.lookup_component("flag2") assert isinstance(flag2, StructureType.ComponentType) assert flag2.initial_value.value == "1" with pytest.raises(TypeError) as err: - stype.add(1, "hello", "hello", None) + stype.add_component(1, "hello", "hello", None) assert ("name of a component of a StructureType must be a 'str' but got " "'int'" in str(err.value)) with pytest.raises(TypeError) as err: - stype.add("hello", "hello", "hello", None) + stype.add_component("hello", "hello", "hello", None) assert ("type of a component of a StructureType must be a 'DataType' " "or 'DataTypeSymbol' but got 'str'" in str(err.value)) with pytest.raises(TypeError) as err: - stype.add("hello", INTEGER_TYPE, "hello", None) + stype.add_component("hello", INTEGER_TYPE, "hello", None) assert ("visibility of a component of a StructureType must be an instance " "of 'Symbol.Visibility' but got 'str'" in str(err.value)) with pytest.raises(TypeError) as err: - stype.add("hello", INTEGER_TYPE, Symbol.Visibility.PUBLIC, "Hello") + stype.add_component("hello", INTEGER_TYPE, Symbol.Visibility.PUBLIC, + "Hello") assert ("The initial value of a component of a StructureType must be " "None or an instance of 'DataNode', but got 'str'." in str(err.value)) with pytest.raises(KeyError): - stype.lookup("missing") + stype.lookup_component("missing") # Cannot have a recursive type definition with pytest.raises(TypeError) as err: - stype.add("hello", stype, Symbol.Visibility.PUBLIC, None) + stype.add_component("hello", stype, Symbol.Visibility.PUBLIC, None) assert ("attempting to add component 'hello' - a StructureType definition " "cannot be recursive" in str(err.value)) + # Add a procedure component + stype.add_procedure_component("proc1", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, + None) + proc1 = stype.lookup_procedure_component("proc1") + assert not proc1.initial_value + assert isinstance(proc1, StructureType.ComponentType) + with pytest.raises(TypeError) as err: + stype.add_procedure_component(None, None, None, None) + assert ("The name of a procedure component of a StructureType must " + "be a 'str' but got 'NoneType'" in str(err.value)) + with pytest.raises(TypeError) as err: + stype.add_procedure_component("proc2", None, None, None) + assert ("The type of a procedure component of a StructureType must " + "be a 'DataType' but got 'NoneType'" in str(err.value)) + with pytest.raises(TypeError) as err: + stype.add_procedure_component("proc3", INTEGER_TYPE, None, None) + assert ("The visibility of a procedure component of a StructureType must " + "be an instance of 'Symbol.Visibility' but got 'NoneType'" + in str(err.value)) + with pytest.raises(TypeError) as err: + stype.add_procedure_component("proc4", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, "Hello") + assert ("The initial value of a procedure component of a StructureType " + "must be None or an instance of 'Reference' but got 'str'." + in str(err.value)) + with pytest.raises(TypeError) as err: + stype.add_procedure_component("proc5", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, + Reference(DataSymbol("not_a_routine", + INTEGER_TYPE))) + assert ("The initial value of a procedure component of a StructureType " + "must be None or a Reference to a RoutineSymbol but got a " + "Reference to a 'DataSymbol'." in str(err.value)) + def test_create_structuretype(): ''' Test the create() method of StructureType. ''' @@ -939,13 +976,13 @@ def test_create_structuretype(): Literal("1.0", REAL_TYPE)), ("barry", tsymbol, Symbol.Visibility.PUBLIC, None)]) assert len(stype.components) == 3 - george = stype.lookup("george") + george = stype.lookup_component("george") assert isinstance(george, StructureType.ComponentType) assert george.name == "george" assert george.datatype == REAL_TYPE assert george.visibility == Symbol.Visibility.PRIVATE assert george.initial_value.value == "1.0" - barry = stype.lookup("barry") + barry = stype.lookup_component("barry") assert isinstance(barry, StructureType.ComponentType) assert barry.datatype is tsymbol assert barry.visibility == Symbol.Visibility.PUBLIC @@ -958,6 +995,36 @@ def test_create_structuretype(): "type, visibility, initial_value) but found a tuple with 2 " "members: ('george', " in str(err.value)) + stype = StructureType.create([], + [("procedure1", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, None), + ("procedure2", REAL_TYPE, + Symbol.Visibility.PRIVATE, + Reference(RoutineSymbol("my_routine", + REAL_TYPE)))]) + assert len(stype.procedure_components) == 2 + proc1 = stype.lookup_procedure_component("procedure1") + assert isinstance(proc1, StructureType.ComponentType) + assert proc1.name == "procedure1" + assert proc1.datatype == INTEGER_TYPE + assert proc1.visibility == Symbol.Visibility.PUBLIC + assert not proc1.initial_value + proc2 = stype.lookup_procedure_component("procedure2") + assert isinstance(proc2, StructureType.ComponentType) + assert proc2.name == "procedure2" + assert proc2.datatype == REAL_TYPE + assert proc2.visibility == Symbol.Visibility.PRIVATE + assert isinstance(proc2.initial_value, Reference) + assert proc2.initial_value.symbol.name == "my_routine" + with pytest.raises(TypeError) as err: + StructureType.create([], + [("procedure1", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, None), + ("procedure2", REAL_TYPE)]) + assert ("Each procedure component must be specified using a 4-tuple of " + "(name, type, visibility, initial_value) but found a tuple with 2 " + "members: ('procedure2', " in str(err.value)) + def test_structuretype_eq(): '''Test the equality operator of StructureType.''' @@ -996,6 +1063,20 @@ def test_structuretype_eq(): ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE, Literal("1.0", REAL_TYPE)), ("roger", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None)]) + # Different number of procedure components. + assert stype != StructureType.create([ + ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None), + ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE, + Literal("1.0", REAL_TYPE))], [ + ("procedure1", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None)]) + # Different procedure components. + stype.add_procedure_component("procedure2", INTEGER_TYPE, + Symbol.Visibility.PUBLIC, None) + assert stype != StructureType.create([ + ("nancy", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None), + ("peggy", REAL_TYPE, Symbol.Visibility.PRIVATE, + Literal("1.0", REAL_TYPE))], [ + ("procedure1", INTEGER_TYPE, Symbol.Visibility.PUBLIC, None)]) def test_structuretype_replace_symbols(): @@ -1017,3 +1098,95 @@ def test_structuretype_replace_symbols(): table.add(newtsymbol) stype.replace_symbols_using(table) assert stype.components["barry"].datatype is newtsymbol + + +def test_structuretype_extends(): + '''Test the StructureType.extends setter.''' + parent_datatype = DataTypeSymbol("parent_type", StructureType()) + structure_type = StructureType.create([('a', INTEGER_TYPE, + Symbol.Visibility.PUBLIC, None)], + extends=parent_datatype) + assert structure_type.extends is parent_datatype + assert structure_type.extends.name == "parent_type" + + with pytest.raises(TypeError) as err: + structure_type.extends = "invalid" + assert ("The type that a StructureType extends must be a " + "DataTypeSymbol but got 'str'." in str(err.value)) + + +def test_replace_procedure_component_initial_value(): + '''Test the replace_procedure_component_initial_value method of + StructureType.''' + stype = StructureType() + with pytest.raises(TypeError) as excinfo: + stype.replace_procedure_component_initial_value(123, None) + assert ("The name of the procedure component to replace must be a string" + in str(excinfo.value)) + + stype = StructureType() + with pytest.raises(TypeError) as excinfo: + stype.replace_procedure_component_initial_value("old_value", 123) + assert ("The new value for the procedure component must be a Reference" + in str(excinfo.value)) + + stype = StructureType() + symbol = Symbol("dummy") + ref = Reference(symbol) + with pytest.raises(TypeError) as excinfo: + stype.replace_procedure_component_initial_value("old_value", ref) + assert ("The new value for the procedure component must be a Reference to " + "a RoutineSymbol" in str(excinfo.value)) + + # UnsupportedFortranType + stype = StructureType() + unsupported_type = UnsupportedFortranType("procedure, code => routine", + None) + stype.add_procedure_component("code", unsupported_type, + Symbol.Visibility.PUBLIC) + new_routine_symbol = RoutineSymbol("new_routine") + new_ref = Reference(new_routine_symbol) + stype.replace_procedure_component_initial_value("routine", new_ref) + assert (stype.lookup_procedure_component("code").datatype.declaration + == "procedure, code => new_routine") + + # Other datatype + stype = StructureType() + routine_symbol = RoutineSymbol("routine") + old_ref = Reference(routine_symbol) + stype.add_procedure_component("code", UnresolvedType(), + Symbol.Visibility.PUBLIC, old_ref) + new_routine_symbol = RoutineSymbol("new_routine") + new_ref = Reference(new_routine_symbol) + stype.replace_procedure_component_initial_value("routine", new_ref) + assert stype.lookup_procedure_component("code").initial_value == new_ref + + # Multiple procedure components of any type with same initial value + stype = StructureType() + unsupported_type_0 = UnsupportedFortranType("procedure, code_0 => routine", + None) + stype.add_procedure_component("code_0", unsupported_type_0, + Symbol.Visibility.PUBLIC) + unsupported_type_1 = UnsupportedFortranType("procedure, code_1 => routine", + None) + stype.add_procedure_component("code_1", unsupported_type_1, + Symbol.Visibility.PUBLIC) + routine_symbol = RoutineSymbol("routine") + stype.add_procedure_component("code_2", UnresolvedType(), + Symbol.Visibility.PUBLIC, + Reference(routine_symbol)) + stype.add_procedure_component("code_3", UnresolvedType(), + Symbol.Visibility.PUBLIC, + Reference(routine_symbol)) + new_routine_symbol = RoutineSymbol("new_routine") + stype.replace_procedure_component_initial_value( + "routine", + Reference(new_routine_symbol)) + assert (stype.lookup_procedure_component("code_0").datatype.declaration + == "procedure, code_0 => new_routine") + assert (stype.lookup_procedure_component("code_1").datatype.declaration + == "procedure, code_1 => new_routine") + assert (stype.lookup_procedure_component("code_2").initial_value.symbol + == new_routine_symbol) + assert (stype.lookup_procedure_component("code_3").initial_value.symbol + == new_routine_symbol)