Repository: stfc/PSyclone
Pull request #2174 (2023-06-12 23:01:42)

extend UnknownFortranType (closes 2171)
Adds datatype information to UnknownFortranType if it can be determined. This is done by ignoring any unsupported attributes (e.g. pointer) and any initial values and storing the resultant PSyIR datatype as partial datatype information.


Patch:
diff --git a/changelog b/changelog
index 3951e1b97..b52c920e1 100644
--- a/changelog
+++ b/changelog
@@ -503,6 +503,9 @@
 	170) PR #2220 for #2219. Adds 'shared_with' argument to the
 	node.ancestor() method.
 
+	171) PR #2174 for #2171. Add support for obtaining the partial datatype
+	of an UnknownFortranType.
+
 release 2.3.1 17th of June 2022
 
 	1) PR #1747 for #1720. Adds support for If blocks to PSyAD.
diff --git a/src/psyclone/psyGen.py b/src/psyclone/psyGen.py
index 62a96d797..5017ab589 100644
--- a/src/psyclone/psyGen.py
+++ b/src/psyclone/psyGen.py
@@ -51,12 +51,10 @@ from psyclone.f2pygen import (AllocateGen, AssignGen, CallGen, CommentGen,
                               DeclGen, DeallocateGen, DoGen, UseGen)
 from psyclone.parse.algorithm import BuiltInCall
 from psyclone.psyir.backend.fortran import FortranWriter
-from psyclone.psyir.backend.visitor import PSyIRVisitor
 from psyclone.psyir.nodes import (Node, Schedule, Loop, Statement, Container,
                                   Routine, Call, OMPDoDirective)
 from psyclone.psyir.symbols import (ArrayType, DataSymbol, RoutineSymbol,
                                     Symbol, ContainerSymbol, ImportInterface,
-                                    ContainerSymbol, ImportInterface,
                                     ArgumentInterface, DeferredType)
 from psyclone.psyir.symbols.datatypes import UnknownFortranType
 
@@ -1741,9 +1739,13 @@ class CodedKern(Kern):
         container_table = container.symbol_table
         for sym in container_table.datatypesymbols:
             if isinstance(sym.datatype, UnknownFortranType):
-                orig_declaration = sym.datatype.declaration
-                sym.datatype.declaration = orig_declaration.replace(
+                new_declaration = sym.datatype.declaration.replace(
                     orig_kern_name, new_kern_name)
+                # pylint: disable=protected-access
+                sym._datatype = UnknownFortranType(
+                    new_declaration,
+                    partial_datatype=sym.datatype.partial_datatype)
+                # pylint: enable=protected-access
 
     @property
     def modified(self):
diff --git a/src/psyclone/psyir/frontend/fparser2.py b/src/psyclone/psyir/frontend/fparser2.py
index da7675d3d..ad668cc28 100644
--- a/src/psyclone/psyir/frontend/fparser2.py
+++ b/src/psyclone/psyir/frontend/fparser2.py
@@ -2092,6 +2092,78 @@ class Fparser2Reader():
             tsymbol.datatype = UnknownFortranType(str(decl))
             tsymbol.interface = UnknownInterface()
 
+    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.
+
+        :param node: fparser2 node containing the declaration statement.
+        :type node: :py:class:`fparser.two.Fortran2008.Type_Declaration_Stmt`
+            or :py:class:`fparser.two.Fortran2003.Type_Declaration_Stmt`
+        :param scope: PSyIR node in which to insert the symbols found.
+        :type scope: :py:class:`psyclone.psyir.nodes.ScopingNode`
+        :param visibility_map: mapping of symbol names to explicit
+            visibilities.
+        :type visibility_map: dict with str keys and values of type
+            :py:class:`psyclone.psyir.symbols.Symbol.Visibility`
+
+        :returns: a PSyIR datatype, or datatype symbol, containing
+            partial datatype information for the declaration statement
+            in the cases where it is possible to extract this
+            information and None otherwise.
+        :rtype: Optional[:py:class:`psyclone.psyir.symbols.DataType` or
+            :py:class:`psyclone.psyir.symbols.DataTypeSymbol`]
+
+        '''
+        # 1: Remove any initialisation and additional variables. TODO:
+        # This won't be needed when #1419 is implemented (assuming the
+        # implementation supports both assignments and pointer
+        # assignments).
+        entity_decl_list = node.children[2]
+        orig_entity_decl_list = list(entity_decl_list.children[:])
+        entity_decl_list.items = tuple(entity_decl_list.children[0:1])
+        entity_decl = entity_decl_list.children[0]
+        orig_entity_decl_children = list(entity_decl.children[:])
+        if isinstance(entity_decl.children[3], Fortran2003.Initialization):
+            entity_decl.items = (
+                entity_decl.items[0], entity_decl.items[1],
+                entity_decl.items[2], None)
+
+        # 2: Remove any unsupported attributes
+        unsupported_attribute_names = ["pointer", "target"]
+        attr_spec_list = node.children[1]
+        orig_node_children = list(node.children[:])
+        orig_attr_spec_list_children = (list(node.children[1].children[:])
+                                        if attr_spec_list else None)
+        if attr_spec_list:
+            entry_list = []
+            for attr_spec in attr_spec_list.children:
+                if str(attr_spec).lower() not in unsupported_attribute_names:
+                    entry_list.append(attr_spec)
+            if not entry_list:
+                node.items = (node.items[0], None, node.items[2])
+            else:
+                node.items[1].items = tuple(entry_list)
+
+        # Try to parse the modified node.
+        symbol_table = SymbolTable()
+        try:
+            self._process_decln(scope, symbol_table, node,
+                                visibility_map)
+            symbol_name = node.children[2].children[0].children[0].string
+            symbol_name = symbol_name.lower()
+            datatype = symbol_table.lookup(symbol_name).datatype
+        except NotImplementedError:
+            datatype = None
+
+        # Restore the fparser2 parse tree
+        node.items = tuple(orig_node_children)
+        if node.children[1]:
+            node.children[1].items = tuple(orig_attr_spec_list_children)
+        node.children[2].items = tuple(orig_entity_decl_list)
+        node.children[2].children[0].items = tuple(orig_entity_decl_children)
+
+        return datatype
+
     def process_declarations(self, parent, nodes, arg_list,
                              visibility_map=None):
         '''
@@ -2109,19 +2181,19 @@ class Fparser2Reader():
         :param arg_list: fparser2 AST node containing the argument list.
         :type arg_list: :py:class:`fparser.Fortran2003.Dummy_Arg_List`
         :param visibility_map: mapping of symbol names to explicit
-                        visibilities.
-        :type visibility_map: dict with str keys and values of type \
-                        :py:class:`psyclone.psyir.symbols.Symbol.Visibility`
+            visibilities.
+        :type visibility_map: dict with str keys and values of type
+            :py:class:`psyclone.psyir.symbols.Symbol.Visibility`
 
         :raises GenerationError: if an INCLUDE statement is encountered.
-        :raises NotImplementedError: the provided declarations contain \
-                                     attributes which are not supported yet.
-        :raises GenerationError: if the parse tree for a USE statement does \
-                                 not have the expected structure.
-        :raises SymbolError: if a declaration is found for a Symbol that is \
-                    already in the symbol table with a defined interface.
-        :raises InternalError: if the provided declaration is an unexpected \
-                               or invalid fparser or Fortran expression.
+        :raises NotImplementedError: the provided declarations contain
+            attributes which are not supported yet.
+        :raises GenerationError: if the parse tree for a USE statement does
+            not have the expected structure.
+        :raises SymbolError: if a declaration is found for a Symbol that is
+            already in the symbol table with a defined interface.
+        :raises InternalError: if the provided declaration is an unexpected
+            or invalid fparser or Fortran expression.
 
         '''
         if visibility_map is None:
@@ -2241,16 +2313,23 @@ class Fparser2Reader():
                         except KeyError:
                             pass
 
+                        # Try to extract partial datatype information.
+                        datatype = self._get_partial_datatype(
+                            node, parent, visibility_map)
+
                         # If a declaration declares multiple entities, it's
                         # possible that some may have already been processed
                         # successfully and thus be in the symbol table.
                         try:
                             parent.symbol_table.add(
-                                DataSymbol(symbol_name,
-                                           UnknownFortranType(str(node)),
-                                           interface=UnknownInterface(),
-                                           visibility=vis),
+                                DataSymbol(
+                                    symbol_name, UnknownFortranType(
+                                        str(node),
+                                        partial_datatype=datatype),
+                                    interface=UnknownInterface(),
+                                    visibility=vis),
                                 tag=tag)
+
                         except KeyError as err:
                             if len(orig_children) == 1:
                                 raise SymbolError(
diff --git a/src/psyclone/psyir/symbols/datatypes.py b/src/psyclone/psyir/symbols/datatypes.py
index fcb5f9307..3672ff9e7 100644
--- a/src/psyclone/psyir/symbols/datatypes.py
+++ b/src/psyclone/psyir/symbols/datatypes.py
@@ -96,8 +96,8 @@ class UnknownType(DataType, metaclass=abc.ABCMeta):
     This class is abstract and must be subclassed for each language
     supported by the PSyIR frontends.
 
-    :param str declaration_txt: string containing the original variable \
-                                declaration.
+    :param str declaration_txt: the original textual declaration of
+        the symbol.
 
     :raises TypeError: if the supplied declaration_txt is not a str.
 
@@ -108,13 +108,16 @@ class UnknownType(DataType, metaclass=abc.ABCMeta):
                 f"UnknownType constructor expects the original variable "
                 f"declaration as a string but got an argument of type "
                 f"'{type(declaration_txt).__name__}'")
-        self._declaration = None
-        self.declaration = declaration_txt
+        self._declaration = declaration_txt
 
     @abc.abstractmethod
     def __str__(self):
         ''' Abstract method that must be implemented in subclass. '''
 
+    # Note, an UnknownType is immutable so a declaration setter is not
+    # allowed. This is to allow subclasses to extract and provide
+    # parts of the declaration without worrying about their values
+    # becoming invalid due to a change in the original declaration.
     @property
     def declaration(self):
         '''
@@ -124,52 +127,45 @@ class UnknownType(DataType, metaclass=abc.ABCMeta):
         '''
         return self._declaration
 
-    @declaration.setter
-    def declaration(self, value):
-        '''
-        Sets the original declaration that this instance represents.
-
-        :param str value: the original declaration.
-
-        '''
-        self._declaration = value[:]
-
 
 class UnknownFortranType(UnknownType):
-    '''
-    Indicates that a Fortran declaration is not supported by the PSyIR.
+    '''Indicates that a Fortran declaration is not supported by the PSyIR.
+
+    :param str declaration_txt: string containing the original variable
+        declaration.
+    :param partial_datatype: a subset of the unknown type if the
+        subset can form a valid PSyIR datatype.
+    :type partial_datatype: Optional[
+        :py:class:`psyclone.psyir.symbols.DataType` or
+        :py:class:`psyclone.psyir.symbols.DataTypeSymbol`]
 
-    :param str declaration_txt: string containing the original variable \
-                                declaration.
     '''
-    def __init__(self, declaration_txt):
+    def __init__(self, declaration_txt, partial_datatype=None):
         super().__init__(declaration_txt)
         # This will hold the Fortran type specification (as opposed to
         # the whole declaration).
         self._type_text = ""
+        if not isinstance(
+                partial_datatype, (type(None), DataType, DataTypeSymbol)):
+            raise TypeError(
+                f"partial_datatype argument in UnknownFortranType "
+                f"initialisation should be a DataType, DataTypeSymbol, or "
+                f"NoneType, but found '{type(partial_datatype).__name__}'.")
+        # This holds a subset of the type in a datatype if it is
+        # possible to determine enough information to create one.
+        self._partial_datatype = partial_datatype
 
     def __str__(self):
         return f"UnknownFortranType('{self._declaration}')"
 
     @property
-    def declaration(self):
-        '''
-        This useless routine is required so that we can override the associated
-        setter method below.
-        '''
-        return super().declaration
-
-    @declaration.setter
-    def declaration(self, value):
+    def partial_datatype(self):
         '''
-        Sets the original declaration that this instance represents and
-        removes any cached type text.
-
-        :param str value: the original declaration.
-
+        :returns: partial datatype information if it can be determined, \
+            else None.
+        :rtype: Optional[:py:class:`psyclone.symbols.DataType`]
         '''
-        self._declaration = value[:]
-        self._type_text = ""
+        return self._partial_datatype
 
     @property
     def type_text(self):
@@ -178,7 +174,7 @@ class UnknownFortranType(UnknownType):
         parse tree to extract the type information. This is returned in
         text form and also cached.
 
-        TODO #1419 - alter Unknown(Fortran)Type so that it is only the
+        TODO #2137 - alter Unknown(Fortran)Type so that it is only the
         type information that is stored as a string. i.e. remove the name
         of the variable being declared. Once that is done this method
         won't be required.


Test patch:
diff --git a/src/psyclone/tests/psyir/frontend/fparser2_test.py b/src/psyclone/tests/psyir/frontend/fparser2_test.py
index 7a2920f21..156248598 100644
--- a/src/psyclone/tests/psyir/frontend/fparser2_test.py
+++ b/src/psyclone/tests/psyir/frontend/fparser2_test.py
@@ -689,6 +689,73 @@ def test_get_routine_schedules_interface(interface_code, parser):
     assert scheds[1].name.lower() == "dummy_code_64"
 
 
+@pytest.mark.usefixtures("f2008_parser")
+def test_get_partial_datatype():
+    '''Test that the _get_partial_datatype method of Fparser2Reader
+    works as expected.
+
+    '''
+    fake_parent = KernelSchedule("dummy_schedule")
+    processor = Fparser2Reader()
+
+    # Entry in symbol table with unmodified properties.
+    reader = FortranStringReader("integer :: l1")
+    node = Specification_Part(reader).content[0]
+    ids = [id(entry) for entry in walk(node)]
+    datatype = processor._get_partial_datatype(node, fake_parent, {})
+    assert isinstance(datatype, ScalarType)
+    assert datatype.intrinsic is ScalarType.Intrinsic.INTEGER
+    # Check fparser2 tree is unmodified
+    assert ids == [id(entry) for entry in walk(node)]
+
+    # Entry in symbol table with partial information. Example has one
+    # unsupported attribute (and no others) and an unsupported assignment.
+    reader = FortranStringReader("integer, pointer :: l1 => null()")
+    node = Specification_Part(reader).content[0]
+    ids = [id(entry) for entry in walk(node)]
+    datatype = processor._get_partial_datatype(node, fake_parent, {})
+    assert isinstance(datatype, ScalarType)
+    assert datatype.intrinsic is ScalarType.Intrinsic.INTEGER
+    # Check fparser2 tree is unmodified
+    assert ids == [id(entry) for entry in walk(node)]
+
+    # Entry in symbol table with partial information. Example has one
+    # unsupported attribute and one supported attribute.
+    reader = FortranStringReader("real*4, target, dimension(10,20) :: l1")
+    node = Specification_Part(reader).content[0]
+    ids = [id(entry) for entry in walk(node)]
+    datatype = processor._get_partial_datatype(node, fake_parent, {})
+    assert isinstance(datatype, ArrayType)
+    assert datatype.intrinsic is ScalarType.Intrinsic.REAL
+    assert datatype.precision == 4
+    assert datatype.shape[0].upper.value == '10'
+    assert datatype.shape[1].upper.value == '20'
+    # Check fparser2 tree is unmodified
+    assert ids == [id(entry) for entry in walk(node)]
+
+    # No entry in symbol table.
+    # Notice the space before complex keyword. This avoids it being
+    # treated as a comment.
+    reader = FortranStringReader(" complex :: c\n")
+    node = Specification_Part(reader).content[0]
+    ids = [id(entry) for entry in walk(node)]
+    assert not processor._get_partial_datatype(node, fake_parent, {})
+    # Check fparser2 tree is unmodified
+    assert ids == [id(entry) for entry in walk(node)]
+
+    # Multiple variables in the declaration are also supported but are
+    # not used by PSyclone at the moment.
+    reader = FortranStringReader(
+        "integer, pointer :: l1 => null(), l2 => null()")
+    node = Specification_Part(reader).content[0]
+    ids = [id(entry) for entry in walk(node)]
+    datatype = processor._get_partial_datatype(node, fake_parent, {})
+    assert isinstance(datatype, ScalarType)
+    assert datatype.intrinsic is ScalarType.Intrinsic.INTEGER
+    # Check fparser2 tree is unmodified
+    assert ids == [id(entry) for entry in walk(node)]
+
+
 @pytest.mark.usefixtures("f2008_parser")
 def test_process_declarations():
     '''Test that process_declarations method of Fparser2Reader
@@ -796,6 +863,28 @@ def test_process_declarations():
             "interface" in str(error.value))
 
 
+@pytest.mark.usefixtures("f2008_parser")
+def test_process_declarations_unknownfortrantype():
+    '''Test that process_declarations method of Fparser2Reader adds
+    datatype information to an UnknownFortranType by calling the
+    get_partial_datatype method, also from Fparser2Reader.
+
+    '''
+    fake_parent = KernelSchedule("dummy_schedule")
+    symtab = fake_parent.symbol_table
+    processor = Fparser2Reader()
+    reader = FortranStringReader(
+        "integer, pointer :: l1 => null(), l2 => null()")
+    fparser2spec = Specification_Part(reader).content[0]
+    processor.process_declarations(fake_parent, [fparser2spec], [])
+    for varname in ("l1", "l2"):
+        var_symbol = symtab.lookup(varname)
+        assert isinstance(var_symbol.datatype, UnknownFortranType)
+        assert isinstance(var_symbol.datatype.partial_datatype, ScalarType)
+        assert (var_symbol.datatype.partial_datatype.intrinsic is
+                ScalarType.Intrinsic.INTEGER)
+
+
 @pytest.mark.usefixtures("f2008_parser")
 def test_process_declarations_errors():
     '''Test that process_declarations method of Fparser2Reader
diff --git a/src/psyclone/tests/psyir/symbols/datatype_test.py b/src/psyclone/tests/psyir/symbols/datatype_test.py
index 6480958b4..bb6cdf15b 100644
--- a/src/psyclone/tests/psyir/symbols/datatype_test.py
+++ b/src/psyclone/tests/psyir/symbols/datatype_test.py
@@ -43,9 +43,9 @@ from psyclone.errors import InternalError
 from psyclone.psyir.nodes import Literal, BinaryOperation, Reference, \
     Container, KernelSchedule
 from psyclone.psyir.symbols import (
-    ArrayType, DataType, DeferredType, ScalarType, UnknownType,
-    UnknownFortranType, DataSymbol, StructureType, NoType,
-    INTEGER_TYPE, REAL_TYPE, Symbol, DataTypeSymbol, SymbolTable)
+    ArrayType, DataType, DeferredType, ScalarType, UnknownFortranType,
+    DataSymbol, StructureType, NoType, INTEGER_TYPE, REAL_TYPE, Symbol,
+    DataTypeSymbol, SymbolTable)
 
 
 # Abstract DataType class
@@ -565,23 +565,6 @@ def test_arraytype_eq():
     assert data_type1 != ArrayType(iscalar_type, [10, 10])
 
 
-# UnknownType tests
-
-def test_unknown_type_declaration_setter():
-    '''Test the declaration setter/getter in UnknownType. We have to subclass
-    UnknownType as it is virtual.'''
-
-    class HardType(UnknownType):
-        '''Concrete sub-class of UnknownType for testing.'''
-        def __str__(self):
-            return "HardType"
-
-    htype = HardType("real var2;")
-    assert htype.declaration == "real var2;"
-    htype.declaration = "real var;"
-    assert htype.declaration == "real var;"
-
-
 # UnknownFortranType tests
 
 def test_unknown_fortran_type():
@@ -593,8 +576,35 @@ def test_unknown_fortran_type():
             "string but got an argument of type 'int'" in str(err.value))
     decl = "type(some_type) :: var"
     utype = UnknownFortranType(decl)
-    assert str(utype) == "UnknownFortranType('" + decl + "')"
-    assert utype.declaration == decl
+    assert utype._type_text == ""
+    assert utype._partial_datatype is None
+    assert str(utype) == f"UnknownFortranType('{decl}')"
+    assert utype._declaration == decl
+
+
+def test_unknown_fortran_type_optional_arg():
+    '''Check the optional 'partial_datatype' argument of the
+    UnknownFortranType class works as expected. Also check the getter
+    method and the string methods work as expected when
+    partial_datatype information is supplied.
+
+    '''
+    decl = "type(some_type) :: var"
+    with pytest.raises(TypeError) as err:
+        _ = UnknownFortranType(decl, partial_datatype="invalid")
+    assert ("partial_datatype argument in UnknownFortranType initialisation "
+            "should be a DataType, DataTypeSymbol, or NoneType, but found "
+            "'str'." in str(err.value))
+    utype = UnknownFortranType(decl, partial_datatype=None)
+    assert utype._partial_datatype is None
+    assert utype.partial_datatype is None
+
+    utype = UnknownFortranType(
+        decl, partial_datatype=DataTypeSymbol("some_type", DeferredType()))
+    assert isinstance(utype._partial_datatype, DataTypeSymbol)
+    assert isinstance(utype.partial_datatype, DataTypeSymbol)
+    assert utype.partial_datatype.name == "some_type"
+    assert str(utype) == f"UnknownFortranType('{decl}')"
 
 
 def test_unknown_fortran_type_text():
@@ -609,9 +619,6 @@ def test_unknown_fortran_type_text():
     # Calling it a second time should just return the previously cached
     # result.
     assert utype.type_text is text
-    # Changing the declaration text should wipe the cache
-    utype.declaration = decl
-    assert utype.type_text is not text
 
 
 def test_unknown_fortran_type_eq():