diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py index b08c8c9535..f2d929100e 100644 --- a/src/psyclone/psyir/frontend/fparser2.py +++ b/src/psyclone/psyir/frontend/fparser2.py @@ -1589,10 +1589,6 @@ 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() not in ("type", "class"): - raise NotImplementedError( - f"Could not process {type_spec} - declarations " - f"other than 'type' or 'class' are not yet supported.") if isinstance(type_spec.items[1], Fortran2003.Type_Name): type_name = str(type_spec.items[1].string).lower() else: @@ -2032,68 +2028,64 @@ def _process_derived_type_decln(self, parent, decl, visibility_map): contains = contains_blocks[0] # Get all procedures in the CONTAINS section. procedures = walk(contains, Fortran2003.Specific_Binding) - if procedures is None: - # The CONTAINS statement is empty. - raise NotImplementedError( - "Derived-type definition contains an empty " - "CONTAINS statement.") - # 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', 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 - if procedure_name in parent.symbol_table and supported: - procedure_symbol = parent.symbol_table.\ - lookup(procedure_name) - procedure_datatype = procedure_symbol.datatype - else: - procedure_datatype = UnsupportedFortranType( - procedure.string, - None) - - # Get the visibility of the procedure. - procedure_vis = dtype_symbol_vis - 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. - if procedure.items[4] is not None: - initial_value_name = procedure.items[4].string - # Look it up in the symbol table and get its datatype - # or create it if it does not exist. - if initial_value_name in parent.symbol_table: - initial_value_symbol = parent.symbol_table.lookup( - initial_value_name) - if (isinstance(procedure_datatype, - UnsupportedFortranType) - and supported): - procedure_datatype = initial_value_symbol.\ - datatype + if len(procedures) > 0: + # 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 + if procedure_name in parent.symbol_table and supported: + procedure_symbol = parent.symbol_table.\ + lookup(procedure_name) + procedure_datatype = procedure_symbol.datatype else: - initial_value_symbol = RoutineSymbol( - initial_value_name, - UnresolvedType()) - initial_value = Reference(initial_value_symbol) - else: - initial_value = None + procedure_datatype = UnsupportedFortranType( + procedure.string, + None) + + # Get the visibility of the procedure. + procedure_vis = dtype_symbol_vis + 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. + if procedure.items[4] is not None: + initial_value_name = procedure.items[4].string + # Look it up in the symbol table and get its + # datatype or create it if it does not exist. + if initial_value_name in parent.symbol_table: + initial_value_symbol = parent.symbol_table.\ + lookup(initial_value_name) + if (isinstance(procedure_datatype, + UnsupportedFortranType) + and supported): + procedure_datatype = initial_value_symbol.\ + datatype + else: + initial_value_symbol = RoutineSymbol( + initial_value_name, + UnresolvedType()) + initial_value = Reference(initial_value_symbol) + else: + initial_value = None - # Add this procedure as a component of the derived type. - dtype.add_procedure_component(procedure_name, - procedure_datatype, - procedure_vis, - initial_value) + # Add this procedure as a component of the derived type + dtype.add_procedure_component(procedure_name, + procedure_datatype, + procedure_vis, + initial_value) # Re-use the existing code for processing symbols. This needs to # be able to find any symbols declared in an outer scope but diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py index c2fee91175..078f330e9c 100644 --- a/src/psyclone/psyir/symbols/datatypes.py +++ b/src/psyclone/psyir/symbols/datatypes.py @@ -973,7 +973,7 @@ def create(components, procedure_components=None, extends=None): stype.add_component(*component) if procedure_components: for procedure_component in procedure_components: - if len(procedure_components) != 4: + 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) " @@ -1123,15 +1123,15 @@ def add_procedure_component(self, name, datatype, visibility, f"be a 'StructureType' but got '{type(datatype).__name__}'") if not isinstance(visibility, Symbol.Visibility): raise TypeError( - f"The visibility of a component of a StructureType must be " - f"an instance of 'Symbol.Visibility' but got " + 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, DataNode)): raise TypeError( - f"The initial value of a component of a StructureType must " - f"be None or an instance of 'DataNode', but got " - f"'{type(initial_value).__name__}'.") + f"The initial value of a procedure component of a " + f"StructureType must be None or an instance of 'DataNode', " + f"but got '{type(initial_value).__name__}'.") self._procedure_components[name] = self.ComponentType( name, datatype, visibility, initial_value) diff --git a/src/psyclone/tests/psyir/backend/fortran_test.py b/src/psyclone/tests/psyir/backend/fortran_test.py index 28e399c686..08a5aa8c8a 100644 --- a/src/psyclone/tests/psyir/backend/fortran_test.py +++ b/src/psyclone/tests/psyir/backend/fortran_test.py @@ -761,7 +761,63 @@ def test_fw_gen_vardecl_visibility(fortran_writer): def test_fw_gen_proceduredecl(fortran_writer): - pass + '''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")) def test_gen_default_access_stmt(fortran_writer): diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py index b293ccc72c..0b96c4514f 100644 --- a/src/psyclone/tests/psyir/frontend/fparser2_test.py +++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py @@ -581,6 +581,21 @@ def test_process_declarations(): assert isinstance(ptr_sym.datatype, UnsupportedFortranType) assert isinstance(ptr_sym.initial_value, IntrinsicCall) + # 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 + @pytest.mark.usefixtures("f2008_parser") @pytest.mark.parametrize("decln_text", @@ -1173,20 +1188,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, 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) - # Allocatable but with specified extent. This is invalid Fortran but # fparser2 doesn't spot it (see fparser/#229). reader = FortranStringReader("integer, allocatable :: l10(5)") @@ -2563,7 +2564,8 @@ def test_structures(fortran_reader, fortran_writer): " integer, public :: j\n" " end type my_type\n" in result) - # type that extends another type (UnsupportedFortranType) + # type that extends another type, known in the symbol table + # (StructureType) test_code = ( "module test_mod\n" " use kernel_mod, only : kernel_type\n" @@ -2577,13 +2579,35 @@ def test_structures(fortran_reader, fortran_writer): 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) - # 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) + + # type that contains a procedure (StructureType) test_code = ( "module test_mod\n" " type :: test_type\n" @@ -2608,6 +2632,24 @@ def test_structures(fortran_reader, fortran_writer): " procedure, nopass :: test_code\n" " end type test_type\n" in result) + # 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" + "END TYPE my_type\n" in result) + # type that creates an abstract type and contains a procedure # (UnsupportedFortranType) test_code = ( @@ -2634,7 +2676,7 @@ def test_structures(fortran_reader, fortran_writer): " PROCEDURE, NOPASS :: test_code\n" "END TYPE test_type\n" in result) - # type that contains a procedure + # type that contains procedures test_code = ( "module test_mod\n" " use kernel_mod, only : parent_type\n" @@ -2660,14 +2702,6 @@ def test_structures(fortran_reader, fortran_writer): 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 :: test_code\n" - # " procedure, private :: test_code_private\n" - # " procedure, public :: test_code_public\n" - # " end type test_type\n" in result) assert ( " type, extends(parent_type), public :: test_type\n" " integer, public :: i = 1\n" @@ -2677,6 +2711,76 @@ def test_structures(fortran_reader, fortran_writer): " procedure, public :: test_code_public\n" " end type test_type\n" in result) + # 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) + + # 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 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 a77bdfe702..2f1d49293c 100644 --- a/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py +++ b/src/psyclone/tests/psyir/symbols/data_type_symbol_test.py @@ -60,7 +60,7 @@ def test_create_datatypesymbol_wrong_datatype(): sym.datatype = "integer" assert ("datatype of a DataTypeSymbol must be specified using a " "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 diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py index c70f6d7fab..2c82ba0d5e 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,7 +890,8 @@ 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 @@ -929,6 +930,33 @@ def test_structure_type(): 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 'DataNode', but got 'str'." + in str(err.value)) + def test_create_structuretype(): ''' Test the create() method of StructureType. ''' @@ -959,6 +987,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.''' @@ -997,6 +1055,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(): @@ -1018,3 +1090,18 @@ 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))