Source code for psyclone.psyir.nodes.call

# -----------------------------------------------------------------------------
# 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.
# -----------------------------------------------------------------------------
# Authors R. W. Ford, A. R. Porter and S. Siso, STFC Daresbury Lab
# -----------------------------------------------------------------------------

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

# Support for *postponed* type annotations.
from __future__ import annotations

from collections.abc import Iterable
from typing import List, Tuple, Union, Optional

from psyclone.configuration import Config
from psyclone.core import AccessType, VariablesAccessMap
from psyclone.errors import GenerationError, PSycloneError
from psyclone.psyir.nodes.codeblock import CodeBlock
from psyclone.psyir.nodes.container import Container
from psyclone.psyir.nodes.statement import Statement
from psyclone.psyir.nodes.datanode import DataNode
from psyclone.psyir.nodes.reference import Reference
from psyclone.psyir.nodes.routine import Routine
from psyclone.psyir.symbols import (
    DataSymbol,
    DataType,
    DataTypeSymbol,
    GenericInterfaceSymbol,
    RoutineSymbol,
    Symbol,
    SymbolError,
    UnsupportedFortranType,
    UnresolvedType
)
from psyclone.psyir.symbols.datatypes import ArrayType


[docs] class CallMatchingArgumentsNotFound(PSycloneError): '''Exception to signal that matching arguments have not been found for this routine ''' def __init__(self, value): PSycloneError.__init__(self, value) self.value = "CallMatchingArgumentsNotFound: " + str(value)
[docs] class Call(Statement, DataNode): ''' Node representing a Call. This can be found as a standalone statement or an expression. TODO #1437: The combined Statement and Expression implementation is simple but it has some shortcomings that may need to be addressed. :param kwargs: additional keyword arguments provided to the PSyIR node. :type kwargs: unwrapped dict. ''' # Textual description of the node. _children_valid_format = "Reference, [DataNode]*" _text_name = "Call" _colour = "cyan" def __init__(self, **kwargs): super().__init__(**kwargs) # The internal _argument_names list can be inconsistent with # the order of the children. Use the property/methods # internally and/or the _reconcile() method to make consistent # when required. self._argument_names = [] def __eq__(self, other): ''' Checks whether two nodes are equal. Two Call nodes are equal if their routine members are equal. :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.argument_names == other.argument_names return is_eq
[docs] @classmethod def create(cls, routine, arguments=()): '''Create an instance of class cls given valid instances of a routine symbol, and a list of child nodes (or name and node tuple) for its arguments. :param routine: the routine that class cls calls. :type routine: py:class:`psyclone.psyir.symbols.RoutineSymbol` | py:class:`psyclone.psyir.nodes.Reference` :param arguments: optional list of arguments for this call, these can be PSyIR nodes or tuples of string,Node for named arguments. :type arguments: Optional[Iterable[\ Union[:py:class:`psyclone.psyir.nodes.DataNode`,\ Tuple[str, :py:class:`psyclone.psyir.nodes.DataNode`]]]] :returns: an instance of cls. :rtype: :py:class:`psyclone.psyir.nodes.Call` or a subclass thereof. :raises TypeError: if the routine argument is not a RoutineSymbol. :raises GenerationError: if the arguments argument is not an Iterable. ''' if not isinstance(routine, (Reference, RoutineSymbol)): raise TypeError( f"The Call routine argument should be a Reference to a " f"RoutineSymbol or a RoutineSymbol, but " f"found '{type(routine).__name__}'.") if not isinstance(arguments, Iterable): raise GenerationError( f"Call.create 'arguments' argument should be an Iterable but " f"found '{type(arguments).__name__}'.") call = cls() if isinstance(routine, Reference): call.addchild(routine) else: call.addchild(Reference(routine)) if arguments: cls._add_args(call, arguments) return call
@staticmethod def _add_args(call, arguments): '''Internal utility method to add arguments to a call node. These are added as child nodes. :param call: the supplied call node. :type call: :py:class:`psyclone.psyir.nodes.Call` :param arguments: list of arguments for this call, these can be PSyIR nodes or tuples of string,Node for named arguments. :type arguments: Iterable[ Union[:py:class:`psyclone.psyir.nodes.DataNode`, Tuple[str, :py:class:`psyclone.psyir.nodes.DataNode`]]] :raises GenerationError: if the contents of the arguments argument are not in the expected form or of the expected type. ''' for arg in arguments: name = None if isinstance(arg, tuple): if not len(arg) == 2: raise GenerationError( f"If a child of the children argument in create " f"method of Call class is a tuple, it's " f"length should be 2, but found {len(arg)}.") if not isinstance(arg[0], str): raise GenerationError( f"If a child of the children argument in create " f"method of Call class is a tuple, its first " f"argument should be a str, but found " f"{type(arg[0]).__name__}.") name, arg = arg call.append_named_arg(name, arg)
[docs] def append_named_arg(self, name, arg): '''Append a named argument to this call. :param name: the argument name. :type name: Optional[str] :param arg: the argument expression. :type arg: :py:class:`psyclone.psyir.nodes.DataNode` :raises ValueError: if the name argument is already used \ for an existing argument. ''' if name is not None: # Avoid circular import. # pylint: disable=import-outside-toplevel from psyclone.psyir.frontend.fortran import FortranReader FortranReader.validate_name(name) for check_name in self.argument_names: if check_name and check_name.lower() == name.lower(): raise ValueError( f"The value of the name argument ({name}) in " f"'append_named_arg' in the 'Call' node is " f"already used for a named argument.") self._argument_names.append((id(arg), name)) self.children.append(arg)
[docs] def insert_named_arg(self, name, arg, index): '''Insert a named argument to the call. :param name: the argument name. :type name: Optional[str] :param arg: the argument expression. :type arg: :py:class:`psyclone.psyir.nodes.DataNode` :param int index: where in the argument list to insert the \ named argument. :raises ValueError: if the name argument is already used \ for an existing argument. :raises TypeError: if the index argument is the wrong type. ''' if name is not None: # Avoid circular import. # pylint: disable=import-outside-toplevel from psyclone.psyir.frontend.fortran import FortranReader FortranReader.validate_name(name) for check_name in self.argument_names: if check_name and check_name.lower() == name.lower(): raise ValueError( f"The value of the name argument ({name}) in " f"'insert_named_arg' in the 'Call' node is " f"already used for a named argument.") if not isinstance(index, int): raise TypeError( f"The 'index' argument in 'insert_named_arg' in the " f"'Call' node should be an int but found " f"{type(index).__name__}.") self._argument_names.insert(index, (id(arg), name)) # The n'th argument is placed at the n'th+1 children position # because the 1st child is the routine reference self.children.insert(index + 1, arg)
[docs] def replace_named_arg(self, existing_name: str, arg: DataNode): '''Replace one named argument node with another node keeping the same name. :param existing_name: the argument name. :param arg: the argument expression. :raises TypeError: if the name argument is the wrong type. :raises ValueError: if the name argument is already used for an existing argument. ''' if not isinstance(existing_name, str): raise TypeError( f"The 'name' argument in 'replace_named_arg' in the " f"'Call' node should be a string, but found " f"{type(existing_name).__name__}.") index = 0 lname = existing_name.lower() for _, name in self._argument_names: if name is not None and name.lower() == lname: break index += 1 else: raise ValueError( f"The value of the existing_name argument ({existing_name}) " f"in 'replace_named_arg' in the 'Call' node was not found " f"in the existing arguments." ) # The n'th argument is placed at the n'th+1 children position # because the 1st child is the routine reference self.children[index + 1] = arg self._argument_names[index] = (id(arg), existing_name)
@staticmethod def _validate_child(position, child): ''' :param int position: the position to be validated. :param child: a child to be validated. :type child: :py:class:`psyclone.psyir.nodes.Node` :return: whether the given child and position are valid for this node. :rtype: bool ''' if position == 0: return isinstance(child, Reference) return isinstance(child, DataNode)
[docs] def reference_accesses(self) -> VariablesAccessMap: ''' TODO #446 - all arguments that are passed by reference are currently marked as having READWRITE access (unless we know that the routine is PURE). We could do better than this if we have the PSyIR of the called Routine. :returns: a map of all the symbol accessed inside this node, the keys are Signatures (unique identifiers to a symbol and its structure accessors) and the values are AccessSequence (a sequence of AccessTypes). ''' var_accesses = VariablesAccessMap() # The RoutineSymbol has a CALL access. sig, indices_list = self.routine.get_signature_and_indices() # pylint: disable=unidiomatic-typecheck if type(self.symbol) is Symbol: # If the type of the Symbol is unknown then this may be a Call or # an array access so the access is UNKNOWN. var_accesses.add_access(sig, AccessType.UNKNOWN, self.routine) else: var_accesses.add_access(sig, AccessType.CALL, self.routine) # Attempt to find the target of the Call so that the argument intents # can be examined. try: routine = self.get_callee()[0] args = routine.symbol_table.argument_list routine_intents = [arg.interface.access for arg in args] except Exception: routine_intents = None # Continue processing references in any index expressions. for indices in indices_list: for idx in indices: var_accesses.update(idx.reference_accesses()) # pylint: disable=import-outside-toplevel from psyclone.psyir.symbols import ArgumentInterface for idx, arg in enumerate(self.arguments): if isinstance(arg, Reference): # This argument is pass-by-reference. sig, indices_list = arg.get_signature_and_indices() if routine_intents: if routine_intents[idx] == ArgumentInterface.Access.WRITE: access_type = AccessType.WRITE elif routine_intents[idx] == ArgumentInterface.Access.READ: access_type = AccessType.READ else: access_type = AccessType.READWRITE else: # We haven't resolved the target of the Call so arguments # default to having the READWRITE (worst-case) access. access_type = AccessType.READWRITE var_accesses.add_access(sig, access_type, arg) # Continue processing references in any index expressions. for indices in indices_list: for idx in indices: var_accesses.update(idx.reference_accesses()) else: # This argument is not a Reference so continue to walk down the # tree. (e.g. it could be/contain a Call to # an impure routine in which case any arguments to that Call # will have READWRITE access.) var_accesses.update(arg.reference_accesses()) return var_accesses
@property def symbol(self) -> Optional[Symbol]: ''' :returns: the routine symbol that this call calls. ''' if self.routine and self.routine.symbol: return self.routine.symbol # In case of incomplete Calls (without mandatory children), return None return None @property def routine(self) -> Optional[Reference]: ''' :returns: the routine reference that this call calls. ''' if len(self._children) >= 1: return self.children[0] return None @property def datatype(self) -> DataType: ''' :returns: the return value type of this call. ''' if self.routine: if isinstance(self.routine.symbol, RoutineSymbol): dt = self.routine.symbol.datatype # We only return the type if it has been resolved, otherwise # we give a chance to the super() method as it can use location # based knowledge to infer its type if not isinstance(dt, UnresolvedType): return dt return super().datatype @property def arguments(self) -> Tuple[DataNode, ...]: ''' :returns: the children of this node that represent its arguments. ''' if len(self._children) >= 2: return tuple(self.children[1:]) return ()
[docs] def argument_by_name(self, name: str) -> Union[DataNode, None]: ''' :param name: The name of the argument to lookup. :returns: The argument specified with the input name, or None if its not present. ''' arg_names = [arg.lower() if arg is not None else None for arg in self.argument_names] lname = name.lower() if lname not in arg_names: return None return self.arguments[arg_names.index(lname)]
@property def is_elemental(self) -> Optional[bool]: ''' :returns: whether the routine being called is elemental (provided with an input array it will apply the operation individually to each of the array elements and return an array with the results). If this information is not known then it returns None. ''' if self.symbol and isinstance(self.symbol, RoutineSymbol): return self.symbol.is_elemental # In case of incomplete Calls (without mandatory children), return None return None @property def is_pure(self) -> Optional[bool]: ''' :returns: whether the routine being called is pure (guaranteed to return the same result when provided with the same argument values). If this information is not known then it returns None. ''' if self.symbol and isinstance(self.symbol, RoutineSymbol): return self.symbol.is_pure # In case of incomplete Calls (without mandatory children), return None return None
[docs] def is_available_on_device(self, device_string: str = "") -> bool: ''' :param device_string: optional string to identify the offloading device (or its compiler-platform family). :returns: whether this call is available on an accelerated device. ''' # pylint: disable=unused-argument return False
@property def argument_names(self): ''' :returns: a list with the name of each argument. If the entry is \ None then the argument is a positional argument. :rtype: List[Optional[str]] ''' self._reconcile() return [entry[1] for entry in self._argument_names] def _reconcile(self): '''Update the _argument_names values in case child arguments have been removed, added, or re-ordered. ''' new_argument_names = [] for child in self.arguments: for arg in self._argument_names: if id(child) == arg[0]: new_argument_names.append(arg) break else: new_argument_names.append((id(child), None)) self._argument_names = new_argument_names
[docs] def node_str(self, colour=True): ''' Construct a text representation of this node, optionally containing colour control codes. :param bool colour: whether or not to include colour control codes. :returns: description of this PSyIR node. :rtype: str ''' return (f"{self.coloured_name(colour)}" f"[name='{self.routine.debug_string()}']")
def __str__(self): return self.node_str(False)
[docs] def copy(self): '''Return a copy of this node. This is a bespoke implementation for a Call node that ensures that any internal id's are consistent before and after copying. :returns: a copy of this node and its children. :rtype: :py:class:`psyclone.psyir.node.Call` ''' # ensure _argument_names is consistent with actual arguments # before copying. self._reconcile() # copy new_copy = super().copy() # Fix invalid id's in _argument_names after copying. # pylint: disable=protected-access new_list = [] for idx, child in enumerate(new_copy.arguments): my_tuple = (id(child), new_copy._argument_names[idx][1]) new_list.append(my_tuple) new_copy._argument_names = new_list return new_copy
[docs] def get_callees(self) -> List[Routine]: ''' Searches for the implementation(s) of all potential target routines for this Call. It does *not* attempt to resolve static polymorphism by checking the argument types. :returns: the Routine(s) that this call targets. :raises NotImplementedError: if the routine is not found or a limitation prevents definite determination of the target routine. ''' rsym = self.routine.symbol if rsym.is_unresolved: # Search for the Routine in the current file. This search is # stopped if we encounter a wildcard import that could be # responsible for shadowing the routine name with an external # implementation. table = rsym.find_symbol_table(self) cursor = table.node have_codeblock = False while cursor: # We want to look in both Containers and FileContainers. if isinstance(cursor, Container): routines = [] for name in cursor.resolve_routine(rsym.name): # Since we're looking in the local Container, the # target is permitted to be private. psyir = cursor.find_routine_psyir(name, allow_private=True) if psyir: routines.append(psyir) if routines: return routines if not have_codeblock: have_codeblock = any(isinstance(child, CodeBlock) for child in cursor.children) wildcard_names = [csym.name for csym in cursor.symbol_table.wildcard_imports( scope_limit=cursor)] if wildcard_names: # We haven't yet found an implementation of the Routine # but we have found a wildcard import and that could be # bringing it into scope so we stop searching (the # alternative is to resolve every wildcard import we # encounter and that is very costly). msg = (f"Failed to find the source code of the " f"unresolved routine '{rsym.name}'. It may be " f"being brought into scope from one of " f"{wildcard_names}") if have_codeblock: msg += (" or it may be within a CodeBlock. If it " "isn't, you ") else: msg += ". You " msg += ("may wish to add the appropriate module name " "to the `RESOLVE_IMPORTS` variable in the " "transformation script.") raise NotImplementedError(msg) parent = cursor.parent cursor = parent.scope if parent else None # We haven't found a Routine and nor have we encountered any # wildcard imports. msg = (f"Failed to find the source code of the unresolved routine " f"'{rsym.name}'. There are no wildcard imports that could " f"be bringing it into scope") if have_codeblock: msg += (" but it might be within a CodeBlock. If it isn't " "then it ") else: msg += ". It " msg += ("must be an external routine that is only resolved at " "link time and searching for such routines is not " "supported.") raise NotImplementedError(msg) root_node = self.ancestor(Container) if not root_node: root_node = self.root container = root_node can_be_private = True if rsym.is_import: # Chase down the Container from which the symbol is imported. cursor = rsym # A Routine imported from another Container must be public in that # Container. can_be_private = False while cursor.is_import: csym = cursor.interface.container_symbol if cursor.interface.orig_name: target_name = cursor.interface.orig_name else: target_name = cursor.name try: container = csym.find_container_psyir(local_node=self) except SymbolError: raise NotImplementedError( f"RoutineSymbol '{rsym.name}' is imported from " f"Container '{csym.name}' but the source defining " f"that container could not be found. The module search" f" path is set to {Config.get().include_paths}") if not container: raise NotImplementedError( f"RoutineSymbol '{rsym.name}' is imported from " f"Container '{csym.name}' but the PSyIR for that " f"container could not be generated.") imported_sym = container.symbol_table.lookup(target_name) if imported_sym.visibility != Symbol.Visibility.PUBLIC: # The required Symbol must be shadowed with a PRIVATE # Symbol in this Container. This means that the one we # actually want is brought into scope via a wildcard # import. # TODO #924 - Use ModuleManager to search? raise NotImplementedError( f"RoutineSymbol '{target_name}' is imported from " f"Container '{csym.name}' but that Container defines " f"a private Symbol of the same name. Searching for the" f" Container that defines a public Routine with that " f"name is not yet supported - TODO #924") if not isinstance(imported_sym, RoutineSymbol): # We now know that this is a RoutineSymbol so specialise it # in place. imported_sym.specialise(RoutineSymbol) cursor = imported_sym rsym = cursor root_node = container # At this point, we should have found the PSyIR tree containing the # routine - we just need to locate it. It may be in a Container or # it may be in the parent FileContainer. cursor = container while cursor and isinstance(cursor, Container): routines = [] all_names = cursor.resolve_routine(rsym.name) if isinstance(rsym, GenericInterfaceSymbol): # Although the interface must be public, the routines to which # it points may themselves be private. can_be_private = True for name in all_names: psyir = cursor.find_routine_psyir( name, allow_private=can_be_private) if psyir: routines.append(psyir) if all_names and len(routines) == len(all_names): # We've resolved everything. return routines cursor = cursor.parent if isinstance(root_node, Container): location_txt = f"Container '{root_node.name}'" else: out_lines = root_node.debug_string().split("\n") idx = -1 while not out_lines[idx]: idx -= 1 last_line = out_lines[idx] location_txt = f"code:\n'{out_lines[0]}\n...\n{last_line}'" raise SymbolError( f"Failed to find a Routine named '{rsym.name}' in " f"{location_txt}. This is normally because the routine" f" is within a CodeBlock.")
def _check_argument_type_matches( self, call_arg: DataNode, routine_arg: DataSymbol ) -> None: """Checks whether the supplied call and routine arguments are compatible. This also supports 'optional' arguments by using partial types. :param call_arg: One argument of the call :param routine_arg: One argument of the routine :raises CallMatchingArgumentsNotFound: if the supplied arguments do not match. """ def type_symbols_match(type1: Union[DataTypeSymbol, DataType], type2: Union[DataTypeSymbol, DataType]) -> bool: ''' :returns: True if the two types correspond to DataTypeSymbols with the same name (case insensitive), False otherwise. ''' return (isinstance(type1, DataTypeSymbol) and isinstance(type2, DataTypeSymbol) and (type1.name.lower() == type2.name.lower())) actual_type = call_arg.datatype dummy_type = routine_arg.datatype if isinstance(actual_type, ArrayType) and isinstance(dummy_type, ArrayType): # Arguments must have the same shape. if len(actual_type.shape) != len(dummy_type.shape): call_arg_str = call_arg.debug_string().strip() routine_arg_str = routine_arg.name raise CallMatchingArgumentsNotFound( f"Rank mismatch of call argument '{call_arg_str}' " f"(rank {len(actual_type.shape)}) and routine argument " f"'{routine_arg_str}' (rank {len(dummy_type.shape)})") # Arguments must have the same intrinsic type. if actual_type.intrinsic != dummy_type.intrinsic: if type_symbols_match(actual_type.intrinsic, dummy_type.intrinsic): return call_arg_str = call_arg.debug_string().strip() routine_arg_str = routine_arg.name raise CallMatchingArgumentsNotFound( f"Array argument type mismatch of call argument " f"'{call_arg_str}' ({actual_type.intrinsic}) and routine " f"argument '{routine_arg_str}' ({dummy_type.intrinsic})") return if isinstance(dummy_type, UnsupportedFortranType): # This could be an 'optional' argument. If so, it will have at # least a partial datatype which we can check. if actual_type != dummy_type.partial_datatype: call_arg_str = call_arg.debug_string().strip() routine_arg_str = routine_arg.name raise CallMatchingArgumentsNotFound( f"Argument partial type mismatch of call argument " f"'{call_arg_str}' ({actual_type}) and routine " f"argument '{routine_arg_str}' (" f"{dummy_type.partial_datatype})" ) else: if actual_type != dummy_type: if type_symbols_match(actual_type, dummy_type): return call_arg_str = call_arg.debug_string().strip() routine_arg_str = routine_arg.name raise CallMatchingArgumentsNotFound( f"Argument type mismatch of call argument '{call_arg_str}'" f" ({actual_type}) and routine argument " f"'{routine_arg_str}' ({dummy_type})" )
[docs] def get_argument_map(self, routine: Routine) -> List[int]: '''Return a list of indices mapping from each argument of this call to the corresponding entry in the argument list of the supplied routine. :param routine: the target of this Call. :return: list of integers referring to matching arguments of the supplied routine. :raises CallMatchingArgumentsNotFound: If there was some problem in finding matching arguments. ''' # Create a copy of the list of actual arguments to the routine. # Once an argument has been successfully matched, set it to 'None' routine_argument_list: List[DataSymbol] = ( routine.symbol_table.argument_list[:] ) if len(self.arguments) > len(routine.symbol_table.argument_list): call_str = self.debug_string().strip() raise CallMatchingArgumentsNotFound( f"More arguments in call ('{call_str}')" f" than callee (routine '{routine.name}')" ) ret_arg_idx_list = [] # Iterate over all arguments to the call for call_arg_idx, call_arg in enumerate(self.arguments): call_arg_idx: int call_arg: DataNode # If the associated name is None, it's a positional argument # => Just return the index if the types match if self.argument_names[call_arg_idx] is None: routine_arg = routine_argument_list[call_arg_idx] routine_arg: DataSymbol self._check_argument_type_matches(call_arg, routine_arg) ret_arg_idx_list.append(call_arg_idx) routine_argument_list[call_arg_idx] = None continue # # Next, we handle all named arguments # arg_name = self.argument_names[call_arg_idx] routine_arg_idx = None for routine_arg_idx, routine_arg in enumerate( routine_argument_list ): routine_arg: DataSymbol # Check if argument was already processed if routine_arg is None: continue if arg_name.lower() == routine_arg.name.lower(): self._check_argument_type_matches( call_arg, routine_arg, ) ret_arg_idx_list.append(routine_arg_idx) break else: # It doesn't match => Raise exception raise CallMatchingArgumentsNotFound( f"Named argument '{arg_name}' not found for routine" f" '{routine.name}' in call '{self.debug_string()}'" ) routine_argument_list[routine_arg_idx] = None # # Finally, we check if all left-over arguments are optional arguments # for routine_arg in routine_argument_list: routine_arg: DataSymbol if routine_arg is None: continue # TODO #759: Optional keyword is not yet supported in psyir. # Hence, we use a simple string match. if ", OPTIONAL" not in str(routine_arg.datatype): call_name = self.debug_string().replace("\n", "") raise CallMatchingArgumentsNotFound( f"Argument '{routine_arg.name}' in subroutine " f"'{routine.name}' does not match any in the call " f"'{call_name}' and is not OPTIONAL." ) return ret_arg_idx_list
[docs] def get_callee( self, use_first_callee_and_no_arg_check: bool = False ) -> Tuple[Routine, List[int]]: ''' Searches for the implementation(s) of the target routine for this Call including argument checks. .. warning:: If `use_first_callee_and_no_arg_check` is set to True, the very first implementation of a Routine with a matching name will be returned. In this case, the arguments of the Call and the Routine might not match. :param use_first_callee_and_no_arg_check: whether or not (the default) to just find the first potential callee without checking its arguments. :returns: A tuple of two elements. The first element is the routine that this call targets. The second one a list of arguments providing the information on matching argument indices. :raises NotImplementedError: if the routine is not local and not found in any containers in scope at the call site. ''' routine_list = self.get_callees() if use_first_callee_and_no_arg_check: arg_match_list = list(i for i in range( len(routine_list[0].symbol_table.argument_list))) return (routine_list[0], arg_match_list) err_info_list = [] # Search for the routine matching the right arguments for routine in routine_list: routine: Routine try: arg_match_list = self.get_argument_map(routine) except CallMatchingArgumentsNotFound as err: err_info_list.append(err.value) continue return (routine, arg_match_list) error_msg = "\n".join(err_info_list) call_str = self.debug_string().replace("\n", "") raise CallMatchingArgumentsNotFound( f"No matching routine found for '{call_str}':" "\n" + error_msg )