# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2014-2025, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors R. W. Ford and A. R. Porter, STFC Daresbury Lab
'''This module provides the Alg class and supporting
exception-handling to translate the original algorithm file into one
that can be compiled and linked with the generated PSy code.
'''
# fparser contains classes that are generated at run time.
# pylint: disable=no-name-in-module
from fparser.two.Fortran2003 import (Main_Program, Module, Use_Stmt, Part_Ref,
Subroutine_Subprogram, Call_Stmt, Name,
Section_Subscript_List, Only_List,
Function_Subprogram, Specification_Part)
# pylint: enable=no-name-in-module
from fparser.two.utils import Base, walk
from psyclone.errors import GenerationError, InternalError, PSycloneError
[docs]
class NoInvokesError(PSycloneError):
'''Provides a PSyclone-specific error class for the situation when an
algorithm code contains no invoke calls.
:param str value: the message associated with the error.
'''
def __init__(self, value):
PSycloneError.__init__(self, value)
self.value = "Algorithm Error: "+str(value)
# pylint: disable=too-few-public-methods
[docs]
class Alg:
'''Generate a modified algorithm code for a single algorithm
specification. Takes the parse tree of the algorithm specification
output from the function :func:`psyclone.parse.algorithm.parse`
and an instance of the :class:`psyGen.PSy` class as input. The
latter allows consistent names to be generated between the
algorithm (callng) and psy (callee) layers.
For example:
>>> from psyclone.algorithm.parse import parse
>>> parse_tree, info = parse("argspec.F90")
>>> from psyclone.psyGen import PSy
>>> psy = PSy(info)
>>> from psyclone.alg_gen import Alg
>>> alg = Alg(parse_tree, psy)
>>> print(alg.gen)
:param parse_tree: an object containing a parse tree of the \
algorithm specification which was produced by the function \
:func:`psyclone.parse.algorithm.parse`. Assumes the algorithm \
will be parsed by fparser2 and expects a valid program unit, \
program, module, subroutine or function.
:type parse_tree: :py:class:`fparser.two.utils.Base`
:param psy: an object containing information about the PSy layer.
:type psy: :py:class:`psyclone.psyGen.PSy`
:param str invoke_name: the name that the algorithm layer uses to \
indicate an invoke call. This is an optional argument that \
defaults to the name "invoke".
'''
def __init__(self, parse_tree, psy, invoke_name="invoke"):
self._ast = parse_tree
self._psy = psy
self._invoke_name = invoke_name
@property
def gen(self):
'''
Modifies and returns the algorithm code. 'invoke' calls are replaced
with calls to the corresponding PSy-layer routines and the USE
statements for the kernels that were referenced by each 'invoke' are
removed.
:returns: the modified algorithm specification as an fparser2 \
parse tree.
:rtype: :py:class:`fparser.two.utils.Base`
:raises NoInvokesError: if no 'invoke()' calls are found.
'''
invoked_kernels = set()
idx = 0
# Walk through all statements looking for procedure calls
for statement in walk(self._ast.content, Call_Stmt):
# found a Fortran call statement
call_name = str(statement.items[0])
if call_name.lower() == self._invoke_name.lower():
# The call statement is an invoke
# Work out which kernels are involved so that we can update the
# USE statements later.
arg_spec_list = statement.children[1]
for child in arg_spec_list.children:
# An invoke() can include a 'name=xxx' argument so we have
# to check that we have a Part_Ref
if isinstance(child, Part_Ref):
invoked_kernels.add(child.children[0].tostr().lower())
# Get the PSy callee name and argument list and
# replace the existing algorithm invoke call with
# these.
psy_invoke_info = self._psy.invokes.invoke_list[idx]
new_name = psy_invoke_info.name
new_args = Section_Subscript_List(
", ".join(psy_invoke_info.alg_unique_args))
statement.items = (new_name, new_args)
# The PSy-layer generates a subroutine within a module
# so we need to add a 'use module_name, only :
# subroutine_name' to the algorithm layer.
_adduse(statement, self._psy.name, only=True,
funcnames=[psy_invoke_info.name])
idx += 1
if idx == 0:
raise NoInvokesError(
"Algorithm file contains no invoke() calls: refusing to "
"generate empty PSy code")
# Remove any un-needed imports of the kernels referenced by the removed
# invoke() calls.
_rm_kernel_use_stmts(invoked_kernels, self._ast)
return self._ast
def _rm_kernel_use_stmts(kernels, ptree):
'''
Remove any unneeded imports of the named kernels from the supplied fparser2
parse tree.
:param Set[str] kernels: the names of the kernels that are no longer \
invoked.
:param ptree: the fparser2 parse tree to update.
:type ptree: :py:class:`fparser.two.Fortran2003.Program`
'''
# Setup the various lookup tables that we'll need.
# Map from the module name to the associated Use_Stmt in the
# parse tree.
use_stmt_map = {}
# Map from module name to the list of symbols imported from it.
use_only_list = {}
# Map from symbol name to the name of the module from which it is
# imported.
sym_to_mod_map = {}
for use_stmt in walk(ptree, Use_Stmt):
mod_name = use_stmt.children[2].tostr().lower()
use_stmt_map[mod_name] = use_stmt
use_only_list[mod_name] = None
only = walk(use_stmt, Only_List)
if only:
use_only_list[mod_name] = []
for child in only[0].children:
sym_name = child.tostr().lower()
use_only_list[mod_name].append(sym_name)
sym_to_mod_map[sym_name] = mod_name
# Remove the USE statements for the invoked kernels provided that
# they're not referenced anywhere (apart from USE statements).
all_other_names = set(name.tostr().lower() for name in
walk(ptree.children, Name)
if not isinstance(name.parent, Only_List))
# Update the lists of symbols imported from each module
for kern in kernels:
if kern not in all_other_names and kern in sym_to_mod_map:
# Kernel name is not referenced anywhere but is imported from
# a module (so is not a Built-In).
mod_name = sym_to_mod_map[kern]
use_only_list[mod_name].remove(kern)
# Finally remove those USE statements that used to have symbols
# associated with them but now have none.
for mod, symbols in use_only_list.items():
if symbols == []:
this_use = use_stmt_map[mod]
spec_part = this_use.parent
spec_part.children.remove(this_use)
# fparser currently falls over when asked to create Fortran for an
# empty Specification_Part (fparser/#359). We therefore remove the
# modified Specification_Part entirely if it is now empty.
if not spec_part.children:
spec_part.parent.children.remove(spec_part)
def _adduse(location, name, only=None, funcnames=None):
'''Add a Fortran 'use' statement to an existing fparser2 parse
tree. This will be added at the first valid location before the
current location.
:param location: the current location (node) in the parse tree to which \
to add a USE.
:type location: :py:class:`fparser.two.utils.Base`
:param str name: the name of the use statement.
:param bool only: whether to include the 'only' clause in the use \
statement or not. Defaults to None which will result in only being \
added if funcnames has content and not being added otherwise.
:param funcnames: a list of names to include in the use statement's \
only list. If the list is empty or None then nothing is \
added. Defaults to None.
:type funcnames: list of str
:raises GenerationError: if no suitable enclosing program unit is found \
for the provided location.
:raises NotImplementedError: if the type of parent node is not supported.
:raises InternalError: if the parent node does not have the expected \
structure.
'''
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
if not isinstance(location, Base):
raise GenerationError(
f"alg_gen.py:_adduse: Location argument must be a sub-class of "
f"fparser.two.utils.Base but got: {type(location).__name__}.")
if funcnames:
# funcnames have been provided for the only clause.
if only is False:
# However, the only clause has been explicitly set to False.
raise GenerationError(
"alg_gen.py:_adduse: If the 'funcnames' argument is provided "
"and has content, then the 'only' argument must not be set "
"to 'False'.")
if only is None:
# only has not been specified so set it to True as it is
# required when funcnames has content.
only = True
if only is None:
# only has not been specified and we can therefore infer that
# funcnames is empty or is not provided (as earlier code would
# have set only to True otherwise) so only is not required.
only = False
# Create the specified use statement
only_str = ""
if only:
only_str = ", only :"
my_funcnames = funcnames
if funcnames is None:
my_funcnames = []
use = Use_Stmt(f"use {name}{only_str} {', '.join(my_funcnames)}")
# find the parent program statement containing the specified location
parent_prog_statement = None
current = location
while current:
if isinstance(current, (Main_Program, Module, Subroutine_Subprogram,
Function_Subprogram)):
parent_prog_statement = current
break
current = current.parent
else:
raise GenerationError(
"The specified location is invalid as it has no parent in the "
"parse tree that is a program, module, subroutine or function.")
if not isinstance(parent_prog_statement,
(Main_Program, Subroutine_Subprogram,
Function_Subprogram)):
# We currently only support program, subroutine and function
# as ancestors
raise NotImplementedError(
f"alg_gen.py:_adduse: Unsupported parent code found "
f"'{type(parent_prog_statement)}'. Currently support is limited "
f"to program, subroutine and function.")
if not isinstance(parent_prog_statement.content[1], Specification_Part):
raise InternalError(
f"alg_gen.py:_adduse: The second child of the parent code "
f"(content[1]) is expected to be a specification part but "
f"found '{repr(parent_prog_statement.content[1])}'.")
# add the use statement as the first child of the specification
# part of the program
spec_part = parent_prog_statement.content[1]
spec_part.content.insert(0, use)
# For auto-API documentation generation.
__all__ = ["NoInvokesError", "Alg"]