Source code for psyclone.psyir.nodes.routine

# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2020-2026, 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.
# -----------------------------------------------------------------------------
# Author: A. R. Porter, STFC Daresbury Lab
# Modified by: R. W. Ford, STFC Daresbury Lab
# Modified by: S. Siso, STFC Daresbury Lab
# Modified by: J. Henrichs, Bureau of Meteorology
# Modified by: A. B. G. Chalk, STFC Daresbury Lab
# -----------------------------------------------------------------------------

''' This module contains the Routine node implementation.'''

from __future__ import annotations
from typing import Optional

from fparser.two import Fortran2003
from fparser.two.utils import walk

from psyclone.errors import GenerationError
from psyclone.psyir.nodes.codeblock import CodeBlock
from psyclone.psyir.commentable_mixin import CommentableMixin
from psyclone.psyir.nodes.node import Node
from psyclone.psyir.nodes.schedule import Schedule
from psyclone.psyir.nodes.scoping_node import ScopingNode
from psyclone.psyir.symbols import (
    DataSymbol, DefaultModuleInterface,
    RoutineSymbol, SymbolError, UnresolvedInterface)
from psyclone.psyir.symbols.symbol_table import SymbolTable


[docs] class Routine(Schedule, CommentableMixin): ''' A sub-class of a Schedule that represents a subroutine, function or program unit. :param symbol: the Symbol used to represent this Routine in its Container. :type symbol: :py:class:`psyclone.psyir.symbols.RoutineSymbol` :param Optional[bool] is_program: whether this Routine represents the entry point into a program (e.g. Fortran Program or C main()). Default is False. :param kwargs: additional keyword arguments provided to the super class. :type kwargs: unwrapped dict. :raises TypeError: if any of the supplied arguments are of the wrong type. ''' # Textual description of the node. _children_valid_format = "[Statement]*" _text_name = "Routine" def __init__(self, symbol, is_program=False, **kwargs): if not isinstance(symbol, RoutineSymbol): raise TypeError(f"Routine argument 'symbol' must be present and " f"must be a RoutineSymbol but got " f"'{type(symbol).__name__}'") # These attributes need to be set before anything, as the _symbol # is required for setting the parent links. self._parent = None self._symbol = symbol super().__init__(**kwargs) self._return_symbol = None if not isinstance(is_program, bool): raise TypeError(f"Routine argument 'is_program' must be a bool " f"but got '{type(is_program).__name__}'") self._is_program = is_program # Add the symbol into the routine itself, unless the symbol table # already contains a symbol with that name (e.g. a Function's return # symbol). if self._symbol.name not in self._symbol_table: self.symbol_table.add(self._symbol) def __eq__(self, other): ''' Checks whether two nodes are equal. Two Routine nodes are equal if they have the same name, same return symbol, same properties and the inherited __eq__ is True. :param object other: the object to check equality to. :returns: whether other is equal to self. :rtype: bool ''' is_eq = super().__eq__(other) is_eq = is_eq and self.name == other.name is_eq = is_eq and self.is_program == other.is_program is_eq = is_eq and self.return_symbol == other.return_symbol return is_eq
[docs] @classmethod def create(cls, name: str, symbol_table: Optional[SymbolTable] = None, children: Optional[list[Node]] = None, is_program: bool = False, return_symbol_name: Optional[str] = None, parent: Optional[Node] = None) -> Routine: # pylint: disable=too-many-arguments '''Create an instance of the supplied class given a name, a symbol table and a list of child nodes. This is implemented as a classmethod so that it is able to act as a Factory for subclasses - e.g. it will create a KernelSchedule if called from KernelSchedule.create(). :param name: the name of the Routine (or subclass). :param symbol_table: the symbol table associated with this Routine. :param children: a list of PSyIR nodes contained in the Routine. :param is_program: whether this Routine represents the entry point into a program (e.g. Fortran Program or C main()). Default is False. :param return_symbol_name: name of the symbol that holds the return value of this routine (if any). Must be present in the supplied symbol table. :param parent: the parent Node of the Node being created. :returns: an instance of `cls`. :raises TypeError: if the arguments to the create method are not of the expected type. ''' if not isinstance(name, str): raise TypeError( f"name argument in create method of Routine class " f"should be a string but found '{type(name).__name__}'.") if symbol_table and not isinstance(symbol_table, SymbolTable): raise TypeError( f"symbol_table argument in create method of Routine class " f"should be a SymbolTable but found " f"'{type(symbol_table).__name__}'.") if not children: children = [] if not isinstance(children, list): raise TypeError( f"children argument in create method of Routine class " f"should be a list but found '{type(children).__name__}'.") for child in children: if not isinstance(child, Node): raise TypeError( f"child of children argument in create method of " f"Routine class should be a PSyIR Node but " f"found '{type(child).__name__}'.") if parent is not None and not isinstance(parent, Node): raise TypeError( f"parent argument in create method of Routine class should " f"be a PSyIR Node but found '{type(parent).__name__}'") symbol = RoutineSymbol(name) routine = cls(symbol, is_program=is_program, symbol_table=symbol_table, parent=parent) routine.children = children if return_symbol_name: routine.return_symbol = routine.symbol_table.lookup( return_symbol_name, scope_limit=routine) return routine
[docs] def node_str(self, colour=True): ''' Returns the name of this node with (optional) control codes to generate coloured output in a terminal that supports it. :param bool colour: whether or not to include colour control codes. :returns: description of this node, possibly coloured. :rtype: str ''' return self.coloured_name(colour) + "[name:'" + self.name + "']"
[docs] def check_outer_scope_accesses(self, call, kern_or_call: str, permit_unresolved: bool = True, ignore_non_data_accesses: bool = False): ''' Check for unresolved symbols or for any declared in the outer scope Container of the target routine. :param call: the node representing the call to the routine that is to be inlined. :type call: Union[CodedKern, Call] :param kern_or_call: text appropriate to whether we have a PSyKAl Kernel or a generic routine. :param permit_unresolved: whether or not the presence of unresolved symbols will result in an error being raised. :param ignore_non_data_accesses: ignore unresolved symbols if they do not represent data accesses (e.g. provide type information). :raises SymbolError: if there is an access to an unresolved symbol and `permit_unresolved` is False. :raises SymbolError: if there is an access to a symbol that is declared in the parent scope of this routine. ''' # TODO #2424 - this suffers from the limitation that # VariablesAccessMap does not work with nested scopes. (e.g. 2 # different symbols with the same name but declared in different, # nested scopes will be assumed to be the same symbol). vam = self.reference_accesses() table = self.symbol_table name = self.name for sig in vam.all_signatures: symbol = table.lookup(sig.var_name, otherwise=None) if not symbol: raise SymbolError( f"{kern_or_call} '{name}' contains accesses to " f"'{sig.var_name}' but the origin of this signature is " f"unknown.") if symbol.is_unresolved: routine_wildcards = table.wildcard_imports() # We can't be certain of the origin of this unresolved symbol. if permit_unresolved: continue if (ignore_non_data_accesses and not vam[sig].has_data_access()): continue raise SymbolError( f"{kern_or_call} '{name}' contains accesses to " f"'{symbol.name}' which is unresolved. It is probably " f"brought into scope from one of " f"{[sym.name for sym in routine_wildcards]}. It may be" f" resolved by adding these to RESOLVE_IMPORTS in the " f"transformation script.") if not symbol.is_import and symbol.name not in table: sym_at_call_site = call.scope.symbol_table.lookup( sig.var_name, otherwise=None) if sym_at_call_site is not symbol: raise SymbolError( f"{kern_or_call} '{name}' contains accesses to " f"'{symbol.name}' which is declared in the callee " f"module scope.") # We can't handle a clash between (apparently) different symbols that # share a name but are imported from different containers. routine_arg_list = self.symbol_table.argument_list[:] callsite_scopes = [] cursor = call while cursor.ancestor(ScopingNode): callsite_scopes.append(cursor.ancestor(ScopingNode)) cursor = cursor.ancestor(ScopingNode) for scope in self.walk(ScopingNode): scope_table = scope.symbol_table for callsite_scope in callsite_scopes: table = callsite_scope.symbol_table try: table.check_for_clashes( scope_table, symbols_to_skip=routine_arg_list) except SymbolError as err: raise SymbolError( f"One or more symbols from routine '{name}' " f"cannot be added to the table at the call site. " f"Error was: {err}") from err
[docs] def update_parent_symbol_table(self, new_parent): ''' Update the Routine's new parent's symbol tables with the corresponding RoutineSymbol. :param new_parent: The new parent of this node. :type new_parent: :py:class:`psyclone.psyir.nodes.ScopingNode` :raises GenerationError: if a symbol with the same name already exists in the scope. :raises GenerationError: if a Routine with the same name already exists in the scope. :raises GenerationError: if a Codeblock representing a routine with the same name already exists in the scope. ''' if self._parent is not None: try: # TODO 2702: Need to check that this Routine's symbol is in # the current _parent symbol table, as otherwise this breaks # during copying. if (self._parent.symbol_table.lookup(self.name) is self._symbol): self._parent.symbol_table.remove(self._symbol) except ValueError: # It can't be removed so we make it Unresolved. This will be # undone when a Routine is re-attached. self._symbol.interface = UnresolvedInterface() except KeyError: pass elif new_parent is not None: # If the current parent is None and the new parent is not None # then we remove the RoutineSymbol from the Routine's symbol table # if it is present. try: if self.symbol_table.lookup(self.name) is self._symbol: self.symbol_table.remove(self._symbol) except KeyError: pass if new_parent is not None: # If we weren't able to remove this symbol from a previous symbol # table then we may need to create a new symbol for this Routine. # The symbol may also be in scope, if it is then we check # whether the scope already has a Routine or CodeBlock # with this name and error if so. try: sym = new_parent.symbol_table.lookup(self.name, scope_limit=new_parent) # If the found symbol is resolved and is not the symbol used # to initialise this Routine then we raise an error, as we # won't be able to add it to the parent. if sym is not self._symbol and not sym.is_unresolved: raise GenerationError( f"Can't add routine '{self.name}' into a scope " f"that already contains a resolved symbol with " f"the same name.") # Check that the scope doesn't contain a Routine or # CodeBlock representing a Routine with this name. routines = new_parent.walk(Routine) for routine in routines: # Ignore itself. if routine is self: continue if routine.name == self.name: raise GenerationError( f"Can't add routine '{self.name}' into a " f"scope that already contains a Routine " f"with that name.") codeblocks = new_parent.walk(CodeBlock) for codeblock in codeblocks: routines = walk(codeblock.parse_tree_nodes, (Fortran2003.Subroutine_Subprogram, Fortran2003.Function_Subprogram)) for routine in routines: name = str(routine.children[0].children[1]) if name == self.name: raise GenerationError( f"Can't add routine '{self.name}' into" f" a scope that already contains a " f"CodeBlock representing a routine " f"with that name.") except KeyError: sym = self._symbol # If lookup found this Routine's symbol then we are performing # replace_with, which is handled here. if sym is self._symbol: try: new_parent.symbol_table.lookup(self._symbol.name, scope_limit=new_parent) except KeyError: new_parent.symbol_table.add(self._symbol) # As we now have the RoutineSymbol back in a Container, we # can give it the right interface. self._symbol.interface = DefaultModuleInterface() elif self.symbol_table: # Otherwise if new_parent is None, then we place the symbol # into this Routine's symbol table if possible. Not all Routine # subclasses have a symbol table, hence the elif statement. try: self.symbol_table.lookup(self._symbol.name) except KeyError: self.symbol_table.add(self._symbol)
@property def dag_name(self): ''' :returns: the name of this node in the dag. :rtype: str ''' return "_".join(["routine", self.name, str(self.START_POSITION)]) @property def name(self): ''' :returns: the name of this Routine. :rtype: str ''' return self._symbol.name @name.setter def name(self, new_name): ''' Sets a new name for the Routine. TODO #1200 this node should only hold a reference to the corresponding RoutineSymbol and get its name from there. :param str new_name: new name for the Routine. :raises TypeError: if new_name is not a string. :raises KeyError: if the symbol is not in the expected symbol table. ''' if not isinstance(new_name, str): raise TypeError(f"Routine name must be a str but got " f"'{type(new_name).__name__}'") if self.name != new_name: symbol = self._symbol if self.parent is not None: self.parent.symbol_table.rename_symbol(symbol, new_name) else: # Check if the symbol in our own symbol table is the symbol # During a copy it is possible for a Routine to not yet # have a SymbolTable so allow for that. if self.symbol_table: try: sym = self.symbol_table.lookup(symbol.name) if sym is self._symbol: self.symbol_table.rename_symbol(symbol, new_name) except KeyError: # Symbol isn't in a symbol table so we can modify its # name freely symbol._name = new_name else: symbol._name = new_name def __str__(self): result = self.node_str(False) + ":\n" for entity in self._children: result += str(entity) + "\n" result += "End " + self.coloured_name(False) return result @property def symbol(self): ''' :returns: the RoutineSymbol corresponding to this Routine. :rtype: :py:class:`psyclone.psyir.symbols.RoutineSymbol` or NoneType ''' return self._symbol @symbol.setter def symbol(self, symbol): ''' :param symbol: the RoutineSymbol corresponding to this Routine. :type symbol: :py:class:`psyclone.psyir.symbols.RoutineSymbol` or NoneType :raises TypeError: if the provided symbol is neither a RoutineSymbol or None ''' if symbol and not isinstance(symbol, RoutineSymbol): raise TypeError(f"Routine symbol must be a RoutineSymbol but got " f"'{type(symbol).__name__}'") self._symbol = symbol @property def is_program(self): ''' :returns: whether this Routine represents the entry point into a \ program (e.g. is a Fortran Program or a C main()). :rtype: bool ''' return self._is_program @property def return_symbol(self): ''' :returns: the symbol which will hold the return value of this Routine \ or None if the Routine is not a function. :rtype: :py:class:`psyclone.psyir.symbols.DataSymbol` or NoneType ''' return self._return_symbol @return_symbol.setter def return_symbol(self, value): ''' Setter for the return-symbol of this Routine node. :param value: the symbol holding the value that the routine returns. :type value: :py:class:`psyclone.psyir.symbols.DataSymbol` :raises TypeError: if the supplied value is not a DataSymbol. :raises KeyError: if the supplied symbol is not a local entry in the \ symbol table of this Routine. ''' if not isinstance(value, DataSymbol): raise TypeError(f"Routine return-symbol should be a DataSymbol " f"but found '{type(value).__name__}'.") if value not in self.symbol_table.datasymbols: raise KeyError( f"For a symbol to be a return-symbol, it must be present in " f"the symbol table of the Routine " f"but '{value.name}' is not.") self._return_symbol = value def _refine_copy(self, other): ''' Refine the object attributes when a shallow copy is not the most appropriate operation during a call to the copy() method. :param other: object we are copying from. :type other: :py:class:`psyclone.psyir.node.Routine` ''' self._symbol = other._symbol.copy() super()._refine_copy(other) if other.return_symbol is not None: self.return_symbol = self.symbol_table.lookup( other.return_symbol.name)
[docs] def replace_with(self, node, keep_name_in_context=True): '''Removes self and its descendants from the PSyIR tree to which it is connected and replaces it with the supplied node (and its descendants). The node must be a Routine (or subclass) and has the same Symbol as self. keep_name_in_context is ignored for this replace_with implementation, however we keep the argument to match with the base implementation. :param node: the node that will replace self in the PSyIR tree. :type node: :py:class:`psyclone.psyir.nodes.Routine` :param bool keep_name_in_context: ignored. :raises TypeError: if the argument node is not a Routine. :raises GenerationError: if this node does not have a parent. :raises GenerationError: if the argument 'node' has a parent :raises GenerationError: if self and node do not have the same Symbol. ''' if not isinstance(node, Routine): raise TypeError( f"The argument node in method replace_with in the Routine " f"class should be a Routine but found '{type(node).__name__}'." ) if not self.parent: raise GenerationError( "This node should have a parent if its replace_with method " "is called.") if node.parent is not None: raise GenerationError( f"The parent of argument node in method replace_with in the " f"Routine class should be None but found " f"'{type(node.parent).__name__}'.") if self._symbol != node._symbol: raise GenerationError( "The symbol of argument node in method replace_with in the " "Routine class should be the same as the Routine being " "replaced.") super().replace_with(node, keep_name_in_context=keep_name_in_context)
# For automatic documentation generation __all__ = ["Routine"]