Example #1
0
def test_call_noargs():
    '''Test that fparser2reader transforms a Fortran subroutine call with
    no arguments into the equivalent PSyIR Call node. Also test that a
    new RoutineSymbol is added to the symbol table (with an unresolved
    interface) when one does not already exist. Also test that the
    Call node ast property is set to reference the original fparser2
    call node.

    '''
    reader = FortranStringReader(" call kernel()")
    ast = Fortran2003.Call_Stmt(reader)
    fake_parent = Schedule()
    processor = Fparser2Reader()
    processor.process_nodes(fake_parent, [ast])

    call_node = fake_parent.children[0]
    assert isinstance(call_node, Call)
    assert not call_node.children

    routine_symbol = call_node.routine
    assert isinstance(routine_symbol, RoutineSymbol)
    assert isinstance(routine_symbol.interface, UnresolvedInterface)
    assert routine_symbol.name == "kernel"
    assert routine_symbol in call_node.scope.symbol_table.symbols

    assert (str(call_node)) == "Call[name='kernel']"

    assert call_node.ast == ast
Example #2
0
    def update(self):
        '''
        Update the underlying fparser2 parse tree to implement the profiling
        region represented by this Node. This involves adding the necessary
        module use statement as well as the calls to the profiling API.

        TODO #435 - remove this whole method once the NEMO API uses the
        Fortran backend of the PSyIR.

        :raises NotImplementedError: if the routine which is to have \
                             profiling added to it does not already have a \
                             Specification Part (i.e. some declarations).
        :raises NotImplementedError: if there would be a name clash with \
                             existing variable/module names in the code to \
                             be transformed.
        :raises InternalError: if we fail to find the node in the parse tree \
                             corresponding to the end of the profiling region.

        '''
        from fparser.common.sourceinfo import FortranFormat
        from fparser.common.readfortran import FortranStringReader
        from fparser.two.utils import walk_ast
        from fparser.two import Fortran2003
        from psyclone.psyGen import object_index, Schedule, InternalError

        # Ensure child nodes are up-to-date
        super(ProfileNode, self).update()

        # Get the parse tree of the routine containing this region
        ptree = self.root.invoke._ast
        # Rather than repeatedly walk the tree, we do it once for all of
        # the node types we will be interested in...
        node_list = walk_ast([ptree], [
            Fortran2003.Main_Program, Fortran2003.Subroutine_Stmt,
            Fortran2003.Function_Stmt, Fortran2003.Specification_Part,
            Fortran2003.Use_Stmt, Fortran2003.Name
        ])
        for node in node_list:
            if isinstance(
                    node,
                (Fortran2003.Main_Program, Fortran2003.Subroutine_Stmt,
                 Fortran2003.Function_Stmt)):
                names = walk_ast([node], [Fortran2003.Name])
                routine_name = str(names[0]).lower()
                break

        for node in node_list:
            if isinstance(node, Fortran2003.Specification_Part):
                spec_part = node
                break
        else:
            # This limitation will be removed when we use the Fortran
            # backend of the PSyIR (#435)
            raise NotImplementedError(
                "Addition of profiling regions to routines without any "
                "existing declarations is not supported and '{0}' has no "
                "Specification-Part".format(routine_name))

        # Get the existing use statements
        found = False
        for node in node_list[:]:
            if isinstance(node, Fortran2003.Use_Stmt) and \
               self.fortran_module == str(node.items[2]).lower():
                # Check that the use statement matches the one we would
                # insert (i.e. the code doesn't already contain a module
                # with the same name as that used by the profiling API)
                if str(node).lower() != self.use_stmt.lower():
                    raise NotImplementedError(
                        "Cannot add profiling to '{0}' because it already "
                        "'uses' a module named '{1}'".format(
                            routine_name, self.fortran_module))
                found = True
                # To make our check on name clashes below easier, remove
                # the Name nodes associated with this use from our
                # list of nodes.
                names = walk_ast([node], [Fortran2003.Name])
                for name in names:
                    node_list.remove(name)

        if not found:
            # We don't already have a use for the profiling module so
            # add one.
            reader = FortranStringReader(
                "use profile_mod, only: ProfileData, ProfileStart, ProfileEnd")
            # Tell the reader that the source is free format
            reader.set_format(FortranFormat(True, False))
            use = Fortran2003.Use_Stmt(reader)
            spec_part.content.insert(0, use)

        # Check that we won't have any name-clashes when we insert the
        # symbols required for profiling. This check uses the list of symbols
        # that we created before adding the `use profile_mod...` statement.
        if not self.root.profiling_name_clashes_checked:
            for node in node_list:
                if isinstance(node, Fortran2003.Name):
                    text = str(node).lower()
                    # Check for the symbols we import from the profiling module
                    for symbol in self.profiling_symbols:
                        if text == symbol.lower():
                            raise NotImplementedError(
                                "Cannot add profiling to '{0}' because it "
                                "already contains a symbol that clashes with "
                                "one of those ('{1}') that must be imported "
                                "from the PSyclone profiling module.".format(
                                    routine_name, symbol))
                    # Check for the name of the profiling module itself
                    if text == self.fortran_module:
                        raise NotImplementedError(
                            "Cannot add profiling to '{0}' because it already "
                            "contains a symbol that clashes with the name of "
                            "the PSyclone profiling module ('profile_mod')".
                            format(routine_name))
                    # Check for the names of profiling variables
                    if text.startswith(self.profiling_var):
                        raise NotImplementedError(
                            "Cannot add profiling to '{0}' because it already"
                            " contains symbols that potentially clash with "
                            "the variables we will insert for each profiling "
                            "region ('{1}*').".format(routine_name,
                                                      self.profiling_var))
        # Flag that we have now checked for name clashes so that if there's
        # more than one profiling node we don't fall over on the symbols
        # we've previous inserted.
        self.root.profiling_name_clashes_checked = True

        # Create a name for this region by finding where this profiling
        # node is in the list of profiling nodes in this Invoke.
        sched = self.root
        pnodes = sched.walk(ProfileNode)
        region_idx = pnodes.index(self)
        region_name = "r{0}".format(region_idx)
        var_name = "psy_profile{0}".format(region_idx)

        # Create a variable for this profiling region
        reader = FortranStringReader(
            "type(ProfileData), save :: {0}".format(var_name))
        # Tell the reader that the source is free format
        reader.set_format(FortranFormat(True, False))
        decln = Fortran2003.Type_Declaration_Stmt(reader)
        spec_part.content.append(decln)

        # Find the parent in the parse tree - first get a pointer to the
        # AST for the content of this region.
        if isinstance(self.children[0], Schedule) and \
           not self.children[0].ast:
            # TODO #435 Schedule should really have a valid ast pointer.
            content_ast = self.children[0][0].ast
        else:
            content_ast = self.children[0].ast
        # Now store the parent of this region
        fp_parent = content_ast._parent
        # Find the location of the AST of our first child node in the
        # list of child nodes of our parent in the fparser parse tree.
        ast_start_index = object_index(fp_parent.content, content_ast)
        # Finding the location of the end is harder as it might be the
        # end of a clause within an If or Select block. We therefore
        # work back up the fparser2 parse tree until we find a node that is
        # a direct child of the parent node.
        ast_end_index = None
        if self.children[-1].ast_end:
            ast_end = self.children[-1].ast_end
        else:
            ast_end = self.children[-1].ast
        # Keep a copy of the pointer into the parse tree in case of errors
        ast_end_copy = ast_end

        while ast_end_index is None:
            try:
                ast_end_index = object_index(fp_parent.content, ast_end)
            except ValueError:
                # ast_end is not a child of fp_parent so go up to its parent
                # and try again
                if hasattr(ast_end, "_parent") and ast_end._parent:
                    ast_end = ast_end._parent
                else:
                    raise InternalError(
                        "Failed to find the location of '{0}' in the fparser2 "
                        "Parse Tree:\n{1}\n".format(str(ast_end_copy),
                                                    str(fp_parent.content)))

        # Add the profiling-end call
        reader = FortranStringReader("CALL ProfileEnd({0})".format(var_name))
        # Tell the reader that the source is free format
        reader.set_format(FortranFormat(True, False))
        pecall = Fortran2003.Call_Stmt(reader)
        fp_parent.content.insert(ast_end_index + 1, pecall)

        # Add the profiling-start call
        reader = FortranStringReader(
            "CALL ProfileStart('{0}', '{1}', {2})".format(
                routine_name, region_name, var_name))
        reader.set_format(FortranFormat(True, False))
        pscall = Fortran2003.Call_Stmt(reader)
        fp_parent.content.insert(ast_start_index, pscall)
Example #3
0
    def update(self):
        # pylint: disable=too-many-branches, too-many-statements
        # pylint: disable=too-many-locals
        '''
        Update the underlying fparser2 parse tree to implement the PSyData
        region represented by this Node. This involves adding the necessary
        module use statement as well as the calls to the PSyData API.

        TODO #435 - remove this whole method once the NEMO API uses the
        Fortran backend of the PSyIR.

        :raises NotImplementedError: if the routine which is to have \
                             PSyData calls added to it does not already have \
                             a Specification Part (i.e. some declarations).
        :raises NotImplementedError: if there would be a name clash with \
                             existing variable/module names in the code to \
                             be transformed.
        :raises InternalError: if we fail to find the node in the parse tree \
                             corresponding to the end of the PSyData region.

        '''
        # Avoid circular dependencies
        # pylint: disable=import-outside-toplevel
        from psyclone.psyGen import object_index, InvokeSchedule
        from psyclone.psyir.nodes import ProfileNode

        # The update function at this stage only supports profiling
        if not isinstance(self, ProfileNode):
            raise InternalError("PSyData.update is only supported for a "
                                "ProfileNode, not for a node of type {0}."
                                .format(type(self)))

        # Ensure child nodes are up-to-date
        super(PSyDataNode, self).update()

        # Get the parse tree of the routine containing this region
        routine_schedule = self.ancestor(InvokeSchedule)
        ptree = routine_schedule.ast

        # Rather than repeatedly walk the tree, we do it once for all of
        # the node types we will be interested in...
        node_list = walk([ptree], (Fortran2003.Main_Program,
                                   Fortran2003.Subroutine_Stmt,
                                   Fortran2003.Function_Stmt,
                                   Fortran2003.Specification_Part,
                                   Fortran2003.Use_Stmt,
                                   Fortran2003.Name))
        if self._module_name:
            routine_name = self._module_name
        else:
            for node in node_list:
                if isinstance(node, (Fortran2003.Main_Program,
                                     Fortran2003.Subroutine_Stmt,
                                     Fortran2003.Function_Stmt)):
                    names = walk([node], Fortran2003.Name)
                    routine_name = str(names[0]).lower()
                    break

        for node in node_list:
            if isinstance(node, Fortran2003.Specification_Part):
                spec_part = node
                break
        else:
            # This limitation will be removed when we use the Fortran
            # backend of the PSyIR (#435)
            raise NotImplementedError(
                "Addition of PSyData regions to routines without any "
                "existing declarations is not supported and '{0}' has no "
                "Specification-Part".format(routine_name))

        # TODO #703: Rename the PSyDataType instead of
        # aborting.
        # Get the existing use statements
        found = False
        for node in node_list[:]:
            if isinstance(node, Fortran2003.Use_Stmt) and \
               self.fortran_module == str(node.items[2]).lower():
                # Check that the use statement matches the one we would
                # insert (i.e. the code doesn't already contain a module
                # with the same name as that used by the PSyData API)
                if str(node).lower() != self.use_stmt.lower():
                    raise NotImplementedError(
                        "Cannot add PSyData calls to '{0}' because it "
                        "already 'uses' a module named '{1}'".format(
                            routine_name, self.fortran_module))
                found = True
                # To make our check on name clashes below easier, remove
                # the Name nodes associated with this use from our
                # list of nodes.
                names = walk([node], Fortran2003.Name)
                for name in names:
                    node_list.remove(name)

        if not found:
            # We don't already have a use for the PSyData module so
            # add one.
            reader = FortranStringReader(
                "use {0}, only: {1}"
                .format(self.add_psydata_class_prefix("psy_data_mod"),
                        self.type_name))
            # Tell the reader that the source is free format
            reader.set_format(FortranFormat(True, False))
            use = Fortran2003.Use_Stmt(reader)
            spec_part.content.insert(0, use)

        # Check that we won't have any name-clashes when we insert the
        # symbols required for the PSyData API. This check uses the list of
        # symbols that we created before adding the `use psy_data_mod...`
        # statement.
        if not routine_schedule.psy_data_name_clashes_checked:
            for node in node_list:
                if isinstance(node, Fortran2003.Name):
                    text = str(node).lower()
                    # Check for the symbols we import from the PSyData module
                    for symbol in self.imported_symbols:
                        if text == symbol.name.lower():
                            raise NotImplementedError(
                                "Cannot add PSyData calls to '{0}' because it "
                                "already contains a symbol that clashes with "
                                "one of those ('{1}') that must be imported "
                                "from the PSyclone PSyData module.".
                                format(routine_name, symbol.name))
                    # Check for the name of the PSyData module itself
                    if text == self.fortran_module:
                        raise NotImplementedError(
                            "Cannot add PSyData calls to '{0}' because it "
                            "already contains a symbol that clashes with the "
                            "name of the PSyclone PSyData module "
                            "('{1}')". format(routine_name,
                                              self.fortran_module))
                    # Check for the names of PSyData variables
                    if text.startswith(self._psy_data_symbol_with_prefix):
                        raise NotImplementedError(
                            "Cannot add PSyData calls to '{0}' because it "
                            "already contains symbols that potentially clash "
                            "with the variables we will insert for each "
                            "PSyData region ('{1}*').".
                            format(routine_name,
                                   self._psy_data_symbol_with_prefix))
        # Flag that we have now checked for name clashes so that if there's
        # more than one PSyData node we don't fall over on the symbols
        # we've previous inserted.
        # TODO #435 the psy_data_name_clashes_checked attribute only exists
        # for a NemoInvokeSchedule. Since this whole `update()` routine will
        # be removed once we are able to use the PSyIR backend to re-generate
        # NEMO code, the pylint warning is disabled.
        # pylint: disable=attribute-defined-outside-init
        routine_schedule.psy_data_name_clashes_checked = True
        # pylint: enable=attribute-defined-outside-init

        # Create a name for this region by finding where this PSyDataNode
        # is in the list of PSyDataNodes in this Invoke.
        pnodes = routine_schedule.walk(PSyDataNode)
        region_idx = pnodes.index(self)
        if self._region_name:
            region_name = self._region_name
        else:
            region_name = "r{0}".format(region_idx)
        var_name = "{0}{1}".format(self.add_psydata_class_prefix("psy_data"),
                                   region_idx)

        # Create a variable for this PSyData region
        reader = FortranStringReader(
            "type({0}), target, save :: {1}".format(self.type_name, var_name))
        # Tell the reader that the source is free format
        reader.set_format(FortranFormat(True, False))
        decln = Fortran2003.Type_Declaration_Stmt(reader)
        spec_part.content.append(decln)

        # Find the parent in the parse tree - first get a pointer to the
        # AST for the content of this region.
        content_ast = self.psy_data_body.children[0].ast
        # Now store the parent of this region
        fp_parent = content_ast.parent
        # Find the location of the AST of our first child node in the
        # list of child nodes of our parent in the fparser parse tree.
        ast_start_index = object_index(fp_parent.content,
                                       content_ast)
        # Do the same for our last child node
        if self.psy_data_body[-1].ast_end:
            ast_end = self.psy_data_body[-1].ast_end
        else:
            ast_end = self.psy_data_body[-1].ast

        if ast_end.parent is not fp_parent:
            raise InternalError(
                "The beginning ({0}) and end ({1}) nodes of the PSyData "
                "region in the fparser2 parse tree do not have the same "
                "parent.".format(content_ast, ast_end))
        ast_end_index = object_index(fp_parent.content, ast_end)

        # Add the PSyData end call
        reader = FortranStringReader(
            "CALL {0}%PostEnd".format(var_name))
        # Tell the reader that the source is free format
        reader.set_format(FortranFormat(True, False))
        pecall = Fortran2003.Call_Stmt(reader)
        pecall.parent = fp_parent
        fp_parent.content.insert(ast_end_index+1, pecall)

        # Add the PSyData start call
        reader = FortranStringReader(
            "CALL {2}%PreStart('{0}', '{1}', 0, 0)".format(
                routine_name, region_name, var_name))
        reader.set_format(FortranFormat(True, False))
        pscall = Fortran2003.Call_Stmt(reader)
        pscall.parent = fp_parent
        fp_parent.content.insert(ast_start_index, pscall)
        # Set the pointers back into the modified parse tree
        self.ast = pscall
        self.ast_end = pecall
        self.set_region_identifier(routine_name, region_name)
Example #4
0
    def lower_to_language_level(self):
        '''
        Lowers this node (and all children) to language-level PSyIR. The
        PSyIR tree is modified in-place. This ProfileNode is replaced by a
        pair of Fortran-specific CodeBlocks (representing the calls to the
        start and stop procedures) with the body (children) of the ProfileNode
        inserted between them. This use of CodeBlocks means that currently only
        the Fortran backend is capable of producing code representing the
        ProfileNode.

        '''
        for child in self.children:
            child.lower_to_language_level()

        routine_schedule = self.ancestor(Routine)
        if self._module_name:
            routine_name = self._module_name
        else:
            routine_name = routine_schedule.name

        if self._region_name:
            region_name = self._region_name
        else:
            # Create a name for this region by finding where this ProfileNode
            # is in the list of ProfileNodes in this Invoke. We allow for any
            # previously lowered ProfileNodes by checking for CodeBlocks with
            # the "profile-start" annotation.
            pnodes = routine_schedule.walk((ProfileNode, CodeBlock))
            region_idx = 0
            for node in pnodes[0:pnodes.index(self)]:
                if (isinstance(node, ProfileNode) or
                        "profile-start" in node.annotations):
                    region_idx += 1
            region_name = "r{0}".format(region_idx)

        # PSyData end call. Since the PSyIR cannot represent type-bound
        # procedures, this call has to be contained in a CodeBlock.
        reader = FortranStringReader(
            "CALL {0}%PostEnd".format(self._var_name))
        # Tell the reader that the source is free format
        reader.set_format(FortranFormat(True, False))
        pecall = Fortran2003.Call_Stmt(reader)
        end_call = CodeBlock([pecall], CodeBlock.Structure.STATEMENT)
        self.parent.children.insert(self.position+1, end_call)

        # PSyData start call (replaces existing PSyDataNode). Again is a call
        # to a type-bound procedure so must be contained in a CodeBlock.
        reader = FortranStringReader(
            "CALL {2}%PreStart('{0}', '{1}', 0, 0)".format(
                routine_name, region_name, self._var_name))
        reader.set_format(FortranFormat(True, False))
        pscall = Fortran2003.Call_Stmt(reader)
        # We annotate this CodeBlock to record the fact that it represents
        # the start of a profiling region.
        start_call = CodeBlock([pscall], CodeBlock.Structure.STATEMENT,
                               annotations=["profile-start"])

        # Insert the body of the profiled region between the start and
        # end calls
        for child in reversed(self.profile_body.pop_all_children()):
            self.parent.children.insert(self.position+1, child)

        # Finally, replace this ProfileNode with the CodeBlock containing the
        # start call
        self.replace_with(start_call)