# -----------------------------------------------------------------------------
# SPDX-FileCopyrightText: Copyright (c) 2017-2026 Science and Technology
# Facilities Council
# SPDX-License-Identifier: BSD-3-Clause
# See the full LICENSE file in the project root for details.
# -----------------------------------------------------------------------------
''' This module provides the KernelModuleInlineTrans transformation.
TODO #2683 - rename this to {Privatise,Copy,Move}RoutineToLocalContainerTrans
and move it to psyir/transformations/.
'''
import logging
from typing import Any, Optional, Union
import warnings
from psyclone.psyGen import Transformation, CodedKern
from psyclone.psyir.transformations import TransformationError
from psyclone.psyir.symbols import (
ContainerSymbol, GenericInterfaceSymbol, RoutineSymbol, Symbol,
SymbolError)
from psyclone.psyir.nodes import (
Call, Container, FileContainer, IntrinsicCall, Reference, Routine,
ScopingNode)
from psyclone.utils import transformation_documentation_wrapper
[docs]
@transformation_documentation_wrapper
class KernelModuleInlineTrans(Transformation):
''' Brings the routine being called into the same Container as the call
site. For example:
>>> from psyclone.domain.common.transformations import \\
... KernelModuleInlineTrans
>>> from psyclone.psyGen import CodedKern
>>> from psyclone.psyir.frontend.fortran import FortranReader
>>> from psyclone.psyir.nodes import Call
>>>
>>> psyir = FortranReader().psyir_from_source("""
... module one
... contains
... subroutine my_subroutine()
... integer, dimension(10, 10) :: A
... A(:,:) = 0
... end subroutine my_subroutine
... end module one
... module two
... use one, only: my_subroutine
... contains
... subroutine call_it()
... call my_subroutine()
... call my_subroutine()
... end subroutine call_it
... end module two
... """)
>>> call = psyir.walk(Call)[-1]
>>> inline_trans = KernelModuleInlineTrans()
>>> inline_trans.apply(call)
>>>
>>> print(call.parent.parent.debug_string())
module two
use one, only : my_subroutine
implicit none
public
<BLANKLINE>
private :: my_subroutine_inlined_
<BLANKLINE>
contains
subroutine call_it()
<BLANKLINE>
call my_subroutine_inlined_()
call my_subroutine_inlined_()
<BLANKLINE>
end subroutine call_it
subroutine my_subroutine_inlined_()
integer, dimension(10,10) :: a
<BLANKLINE>
a(:,:) = 0
<BLANKLINE>
end subroutine my_subroutine_inlined_
<BLANKLINE>
end module two
<BLANKLINE>
.. warning ::
Not all Routines can be moved. This transformation will reject
attempts to move routines that access private data in the
original Container.
'''
def __str__(self):
return ("Copy the routine associated with a (Kernel) call into the "
"Container of the call site.")
# pylint: disable=too-many-branches
[docs]
def validate(self,
node: Union[CodedKern, Call],
options: Optional[dict[str, Any]] = None,
**kwargs) -> None:
'''
Checks that the supplied node is a Kernel or Call and that it is
possible to copy its PSyIR into the parent Container.
If the target of the supplied (Kernel) Call is already in local
scope then further checks are skipped.
:param node: the kernel or call which is the target of the
transformation.
:param options: a dictionary with options for transformations.
:raises TransformationError: if the target node is not a sub-class of
psyGen.CodedKern or psyir.nodes.Call or is an IntrinsicCall.
:raises TransformationError: if the call is to a polymorphic routine
and there's no Container at the call site to which to add the
interface definition.
:raises TransformationError: if the kernel cannot be safely inlined.
'''
if not options:
self.validate_options(**kwargs)
if isinstance(node, CodedKern):
kern_or_call = "Kernel"
elif isinstance(node, Call):
if isinstance(node, IntrinsicCall):
raise TransformationError(
f"Cannot module-inline a call to an intrinsic (got "
f"'{node.debug_string()}')")
kern_or_call = "routine"
else:
raise TransformationError(
f"Target of a {self.name} must be a sub-class of "
f"psyGen.CodedKern or psyir.nodes.Call but got "
f"'{type(node).__name__}'")
# Check that the PSyIR of the routine/kernel can be retrieved.
kname = node.routine.symbol.name
try:
kernels = node.get_callees()
except Exception as error:
raise TransformationError(
f"{self.name} failed to retrieve PSyIR for {kern_or_call} "
f"'{kname}' due to: {error}"
) from error
# Return early if the target of the (Kernel) Call is already local.
if self._target_is_local(node):
return
if len(kernels) > 1:
# We can't bring the target of a call to an interface into local
# scope if there's no Container in which to put the interface.
cntr = node
while cntr:
cntr = cntr.ancestor(Container)
if cntr and not isinstance(cntr, FileContainer):
break
else:
raise TransformationError(
f"Cannot copy the target of the call to '{kname}' since "
f"it is a polymorphic routine (i.e. an interface) and the "
f"call-site is not within a module.")
# Validate the PSyIR of each routine/kernel.
for kernel_schedule in kernels:
self._validate_schedule(node, kname, kern_or_call, kernel_schedule)
def _validate_schedule(self, node, kname, kern_or_call, kernel_schedule):
'''
Validates that the supplied schedule can be module-inlined.
:param node: the candidate kernel/routine call to inline.
:type node: :py:class:`psyclone.psyGen.CodedKern` |
:py:class:`psyclone.psyir.nodes.Call`
:param str kname: the name of the kernel/routine.
:param str kern_or_call: text for readable error messages.
:param kernel_schedule: the schedule of the routine to inline.
:type kernel_schedule: :py:class:`psyclone.psyir.nodes.Schedule`
:raises TransformationError: if the called routine contains accesses
to data declared in the same module scope or of unknown origin.
:raises TransformationError: if the called routine contains a local
Symbol that shadows a module name in its outer scope.
'''
# We do not support kernels that use symbols representing data
# declared in their own parent module (we would need to add new imports
# from this module at the call site, and we don't do this yet).
try:
kernel_schedule.check_outer_scope_accesses(node, kern_or_call)
except SymbolError as err:
raise TransformationError(
f"Cannot apply {self.name} to {kern_or_call} '{kname}' "
f"because it accesses data from its outer scope: "
f"{err.value}") from err
# We can't transform subroutines that shadow top-level symbol module
# names, because we won't be able to bring them into the subroutine.
# (We could attempt to rename the local symbol.)
symtab = kernel_schedule.ancestor(Container).symbol_table
ctr_names = [sym.name.lower() for sym in symtab.containersymbols]
for scope in kernel_schedule.walk(ScopingNode):
for symbol in scope.symbol_table.symbols:
if (symbol.name.lower() in ctr_names and
not isinstance(symbol, ContainerSymbol)):
raise TransformationError(
f"{kern_or_call} '{kname}' cannot be module-"
f"inlined because the subroutine contains a symbol "
f"'{symbol.name}' which shadows the name of a module "
f"in the outer scope.")
@staticmethod
def _prepare_code_to_inline(
routines_to_inline: list[Routine]) -> list[Routine]:
'''Prepare the PSyIR tree to inline by bringing in to the subroutine
all referenced symbols so that the implementation is self contained.
The provided routines are copied so that the original PSyIR is left
unmodified.
:param routines_to_inline: the routine(s) to module-inline.
:returns: the updated routine(s) to module-inline.
'''
# pylint: disable=too-many-branches
orig_container = routines_to_inline[0].ancestor(Container)
# Since we will be detaching Routines, we work with a copy of
# the Container that encapsulates them.
source_container = orig_container.copy()
new_routines = {}
for routine in source_container.walk(Routine):
new_routines[routine.name] = routine
copied_routines = []
for orig_routine in routines_to_inline:
code_to_inline = new_routines[orig_routine.name]
copied_routines.append(code_to_inline)
vam = code_to_inline.reference_accesses()
# First make a set with all symbols used inside the subroutine
all_symbols = set()
for sig in vam.all_signatures:
all_symbols.add(
code_to_inline.symbol_table.lookup(sig.var_name))
# Decide which symbols need to be brought inside the subroutine
symbols_to_bring_in = set()
for symbol in all_symbols:
if symbol.is_unresolved or symbol.is_import:
# This symbol may already be in the local symbol table,
# but adding it to the 'symbols_to_bring_in' will make the
# next step bring into the subroutine all modules that it
# could come from.
symbols_to_bring_in.add(symbol)
# Bring the selected symbols inside the subroutine
for symbol in symbols_to_bring_in:
if symbol.name not in code_to_inline.symbol_table:
if symbol.is_import:
# We must update its import interface (to ensure it
# references a ContainerSymbol in the correct scope)
# before it can be added to the table.
code_to_inline.symbol_table.\
localise_import_interface_of(symbol)
code_to_inline.symbol_table.add(symbol)
# And when necessary the modules where they come from
if symbol.is_unresolved:
# We don't know where this comes from, we need to bring
# in all top-level imports with wildcard imports
for mod in source_container.symbol_table.containersymbols:
if mod.wildcard_import:
if mod.name not in code_to_inline.symbol_table:
code_to_inline.symbol_table.add(mod)
else:
code_to_inline.symbol_table.lookup(mod.name).\
wildcard_import = True
elif symbol.is_import:
module_symbol = symbol.interface.container_symbol
if module_symbol.name not in code_to_inline.symbol_table:
code_to_inline.symbol_table.add(module_symbol)
else:
# If it already exists, we know it's a container (from
# the validation) so we just need to point to it
symbol.interface.container_symbol = \
code_to_inline.symbol_table.lookup(
module_symbol.name)
return copied_routines
def _target_is_local(self, node: Union[Call, CodedKern]) -> bool:
'''
:returns: whether or not the target of the supplied call/kernel
is already in local scope.
'''
kname = node.routine.symbol.name
routines = node.get_callees()
if len(routines) > 1:
iface_sym = node.scope.symbol_table.lookup(kname, otherwise=None)
if (not iface_sym or (iface_sym.is_import or
iface_sym.is_unresolved)):
return False
for kernel_schedule in routines:
rt_sym = node.scope.symbol_table.lookup(kernel_schedule.name,
otherwise=None)
if (not rt_sym or (rt_sym is not kernel_schedule.symbol) or
(node.ancestor(Container) is not
kernel_schedule.ancestor(Container)) or
(rt_sym.is_import or rt_sym.is_unresolved)):
return False
logger = logging.getLogger(__name__)
logger.info(
f"The target of '{node.debug_string().strip()}' is already "
f"present in the local scope.")
return True
[docs]
def apply(self,
node: Union[CodedKern, Call],
options: dict[str, Any] = None,
update_all: bool = True,
**kwargs):
''' Bring the implementation of this kernel/call into this Container.
NOTE: when applying this transformation to a Kernel in a PSyKAl invoke,
by default *all* calls to that Kernel are updated.
Similarly, when applied to a Call to a Routine in a particular scope,
*all* such Calls are updated. This behaviour may be changed using the
`update_all=False` option.
:param node: the Kernel or Call to module-inline.
:param options: a dictionary with options for transformations.
:param update_all: whether or not to update *all* (kernel) calls to the
target Kernel/routine within the program unit.
'''
if options:
# TODO 2668 - options dict is deprecated.
warnings.warn(self._deprecation_warning, DeprecationWarning, 2)
if not options:
options = {}
self.validate(node, options, **kwargs)
external_callee_name = None
if isinstance(node, CodedKern):
caller_name = node.name
else:
caller_name = node.routine.symbol.name
if (node.routine.symbol.is_import and
node.routine.symbol.interface.orig_name):
external_callee_name = node.routine.symbol.interface.orig_name
if not external_callee_name:
external_callee_name = caller_name
# Get the PSyIR of the routine to module inline as well as the name
# with which it is being called.
# Note that we use the resolved callee subroutine name and not the
# caller one; this is important because if it is an interface it will
# use the concrete implementation name. When this happens the new name
# may already be in use, but the equality check below guarantees
# that if it exists it is only valid when it references the exact same
# implementation.
codes_to_inline = node.get_callees()
interface_sym = None
if len(codes_to_inline) > 1:
interface_sym = codes_to_inline[0].symbol_table.lookup(
external_callee_name)
if self._target_is_local(node):
return
callsite_table = node.scope.symbol_table
if interface_sym:
called_sym = callsite_table.lookup(caller_name, otherwise=None)
else:
for routine in codes_to_inline:
# N.B. in a PSyKAl DSL, we won't have a RoutineSymbol for the
# Kernel that is being called, so we look it up instead of
# using node.symbol.
called_sym = callsite_table.lookup(caller_name,
otherwise=None)
if (not called_sym or called_sym is not routine.symbol or
(called_sym.is_import or called_sym.is_unresolved)):
# This routine is not module-inlined.
break
updated_routines = self._prepare_code_to_inline(codes_to_inline)
# The Container into which we will inline the Routine(s).
container = node.ancestor(Container)
# Mapping from original name to new RoutineSymbol.
name_map: dict[str, RoutineSymbol] = {}
for code_to_inline in updated_routines:
# Create a new name for the routine.
new_name = code_to_inline.name+"_inlined_"
new_sym = container.symbol_table.new_symbol(
new_name, symbol_type=RoutineSymbol)
new_sym.copy_properties(code_to_inline.symbol,
exclude_interface=True)
# Do not expose the new symbol externally to prevent unexpected
# collisions.
new_sym.visibility = Symbol.Visibility.PRIVATE
# Add the new symbol to the map.
name_map[code_to_inline.name] = new_sym
# Add the routine code into this Container
code_to_inline = code_to_inline.detach()
code_to_inline.symbol = new_sym
container.addchild(code_to_inline)
if interface_sym:
# Deal with the interface symbol - create a new, local
# private version.
new_sym = container.symbol_table.new_symbol(
interface_sym.name+"_inlined_",
symbol_type=GenericInterfaceSymbol,
routines=[(sym, True) for sym in name_map.values()],
visibility=Symbol.Visibility.PRIVATE)
name_map[interface_sym.name] = new_sym
if update_all:
# We will update all Calls/Kernels associated with the
# target routine.
all_calls = container.walk(type(node))
else:
# Only update the supplied Call/Kernel.
all_calls = [node]
target_sym = name_map.get(caller_name, None)
if not target_sym:
# If we haven't copied in a routine of 'caller_name' then it must
# be because the target of the call is renamed on import.
target_sym = name_map.get(external_callee_name)
for call in all_calls:
name = call.routine.symbol.name.lower()
if name == caller_name:
if isinstance(node, Call):
call.routine.symbol = target_sym
else:
# Otherwise node is a CodedKern.
call.routine = Reference(target_sym)
call._schedules = updated_routines
# In theory we could remove the import of the original routine but
# that is dangerous (e.g. if it's a subroutine-scoped or
# module-private symbol) and unnecessary so we don't.