# pylint: disable=too-many-lines
# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2017-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, A. R. Porter and S. Siso, STFC Daresbury Lab
# Modified J. Henrichs, Bureau of Meteorology
# Modified I. Kavcic, Met Office
'''This module implements the PSyclone GOcean 1.0 API by specialising
the required base classes for both code generation (PSy, Invokes,
Invoke, InvokeSchedule, Loop, Kern, Arguments and KernelArgument)
and parsing (Descriptor and KernelType). It adds a
GOKernelGridArgument class to capture information on kernel arguments
that supply properties of the grid (and are generated in the PSy
layer).
'''
import re
from fparser.two.Fortran2003 import NoMatchError, Nonlabel_Do_Stmt
from fparser.two.parser import ParserFactory
from psyclone.configuration import Config, ConfigurationError
from psyclone.core import Signature, VariablesAccessMap
from psyclone.domain.common.psylayer import PSyLoop
from psyclone.domain.gocean import GOceanConstants, GOSymbolTable
from psyclone.errors import GenerationError, InternalError
import psyclone.expression as expr
from psyclone.parse.algorithm import Arg
from psyclone.parse.kernel import Descriptor, KernelType
from psyclone.parse.utils import ParseError
from psyclone.psyGen import (
PSy, Invokes, Invoke, InvokeSchedule, CodedKern, Arguments, Argument,
KernelArgument, args_filter, AccessType, HaloExchange)
from psyclone.psyir.frontend.fparser2 import Fparser2Reader
from psyclone.psyir.frontend.fortran import FortranReader
from psyclone.psyir.nodes import (
Literal, Schedule, KernelSchedule, StructureReference, IntrinsicCall,
Reference, Call, Assignment, ACCEnterDataDirective, ACCParallelDirective,
ACCKernelsDirective, Container, ACCUpdateDirective, Routine,
BinaryOperation)
from psyclone.psyir.symbols import (
ImportInterface, INTEGER_TYPE, DataSymbol, RoutineSymbol, ContainerSymbol,
ScalarType, UnresolvedType, DataTypeSymbol, UnresolvedInterface,
BOOLEAN_TYPE, REAL_TYPE)
from psyclone.psyir.tools import DependencyTools
[docs]
class GOPSy(PSy):
'''
The GOcean 1.0 specific PSy class. This creates a GOcean specific
invokes object (which controls all the required invocation calls).
Also overrides the PSy gen method so that we generate GOcean-
specific PSy module code.
:param invoke_info: An object containing the required invocation \
information for code optimisation and generation.
:type invoke_info: :py:class:`psyclone.parse.FileInfo`
'''
def __init__(self, invoke_info):
Config.get().api = "gocean"
PSy.__init__(self, invoke_info)
# Add GOcean infrastructure-specific libraries
field_sym = ContainerSymbol("field_mod")
field_sym.wildcard_import = True
self.container.symbol_table.add(field_sym)
kind_params_sym = ContainerSymbol("kind_params_mod")
kind_params_sym.wildcard_import = True
self.container.symbol_table.add(kind_params_sym)
# Create invokes
self._invokes = GOInvokes(invoke_info.calls, self)
[docs]
class GOInvokes(Invokes):
'''
The GOcean specific invokes class. This passes the GOcean specific
invoke class to the base class so it creates the one we require.
:param alg_calls: The Invoke calls discovered in the Algorithm layer.
:type alg_calls: OrderedDict of :py:class:`psyclone.parse.InvokeCall` \
objects.
:param psy: the PSy object containing this GOInvokes object.
:type psy: :py:class:`psyclone.gocean1p0.GOPSy`
'''
def __init__(self, alg_calls, psy):
Invokes.__init__(self, alg_calls, GOInvoke, psy)
index_offsets = []
# Loop over all of the kernels in all of the invoke() calls
# and check that they work on compatible grid-index offsets.
# Strictly speaking this check should be done in the parsing
# code since it is a check on the correctness of the meta-data.
# However, that would require a fundamental change to the parsing
# code since it requires information on all of the invokes and
# kernels in an application. Therefore it is much simpler to
# do it here where we have easy access to that information.
for invoke in self.invoke_list:
for kern_call in invoke.schedule.coded_kernels():
# We only care if the index offset is not offset_any (since
# that is compatible with any other offset)
if kern_call.index_offset != "go_offset_any":
# Loop over the offsets we've seen so far
for offset in index_offsets:
if offset != kern_call.index_offset:
raise GenerationError(
f"Meta-data error in kernel {kern_call.name}: "
f"INDEX_OFFSET of '{kern_call.index_offset}' "
f"does not match that ({offset}) of other "
f"kernels. This is not supported.")
# Append the index-offset of this kernel to the list of
# those seen so far
index_offsets.append(kern_call.index_offset)
[docs]
class GOInvoke(Invoke):
'''
The GOcean specific invoke class. This passes the GOcean specific
schedule class to the base class so it creates the one we require.
:param alg_invocation: Node in the AST describing the invoke call.
:type alg_invocation: :py:class:`psyclone.parse.InvokeCall`
:param int idx: The position of the invoke in the list of invokes \
contained in the Algorithm.
:param invokes: the Invokes object containing this GOInvoke \
object.
:type invokes: :py:class:`psyclone.gocean1p0.GOInvokes`
'''
def __init__(self, alg_invocation, idx, invokes):
self._schedule = GOInvokeSchedule.create('name')
Invoke.__init__(self, alg_invocation, idx, GOInvokeSchedule, invokes)
if Config.get().distributed_memory:
# Insert halo exchange calls
for loop in self.schedule.loops():
loop.create_halo_exchanges()
[docs]
class GOInvokeSchedule(InvokeSchedule):
''' The GOcean specific InvokeSchedule sub-class. We call the base class
constructor and pass it factories to create GO-specific calls to both
user-supplied kernels and built-ins.
:param symbol: RoutineSymbol representing the Invoke.
:type symbol: :py:class:`psyclone.psyir.symbols.RoutineSymbol`
:param alg_calls: optional list of KernelCalls parsed from the algorithm
layer.
:type alg_calls: Optional[list of
:py:class:`psyclone.parse.algorithm.KernelCall`]
:param parent: the parent of this node in the PSyIR.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
'''
# Textual description of the node.
_text_name = "GOInvokeSchedule"
def __init__(self, symbol, alg_calls=None, parent=None, **kwargs):
if not alg_calls:
alg_calls = []
InvokeSchedule.__init__(self, symbol, GOKernCallFactory,
GOBuiltInCallFactory, alg_calls,
parent=parent, **kwargs)
# pylint: disable=too-many-instance-attributes
[docs]
class GOLoop(PSyLoop):
''' The GOcean specific PSyLoop class. This passes the GOcean specific
single loop information to the base class so it creates the one we
require. Adds a GOcean specific setBounds method which tells the loop
what to iterate over. Need to harmonise with the topology_name method
in the LFRic API.
:param parent: optional parent node (default None).
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:param str loop_type: loop type - must be 'inner' or 'outer'.
:param str field_name: name of the field this loop iterates on.
:param str field_space: space of the field this loop iterates on.
:param str iteration_space: iteration space of the loop.
:raises GenerationError: if the loop is not inserted inside a \
GOInvokeSchedule region.
'''
_bounds_lookup = {}
def __init__(self, parent, loop_type="", field_name="", field_space="",
iteration_space="", index_offset=""):
# pylint: disable=too-many-arguments
const = GOceanConstants()
super().__init__(parent=parent,
valid_loop_types=const.VALID_LOOP_TYPES)
# The following attributes are validated in the respective setters
self.loop_type = loop_type
self.field_name = field_name
self.field_space = field_space
self.iteration_space = iteration_space
self.index_offset = index_offset
# Check that the GOLoop is inside the GOcean PSy-layer
if not self.ancestor(GOInvokeSchedule):
raise GenerationError(
"GOLoops must always be constructed with a parent which is"
" inside (directly or indirectly) of a GOInvokeSchedule")
# We set the loop variable name in the constructor so that it is
# available when we're determining which vars should be OpenMP
# PRIVATE (which is done *before* code generation is performed)
if self.loop_type == "inner":
tag = "contiguous_kidx"
suggested_name = "i"
elif self.loop_type == "outer":
tag = "noncontiguous_kidx"
suggested_name = "j"
else:
raise InternalError(f"While the loop type '{self._loop_type}' is "
f"valid, it is not yet supported.")
# In the GOcean API the loop iteration variables are declared in the
# Invoke routine scope in order to share them between all GOLoops.
# This is important because some transformations/scripts work with
# this assumption when moving or fusing loops.
symtab = self.ancestor(InvokeSchedule).symbol_table
try:
self.variable = symtab.lookup_with_tag(tag)
except KeyError:
self.variable = symtab.new_symbol(
suggested_name, tag, symbol_type=DataSymbol,
datatype=INTEGER_TYPE)
# Initialise bounds lookup map if it is not already
if not GOLoop._bounds_lookup:
GOLoop.setup_bounds()
[docs]
@staticmethod
def create(parent, loop_type, field_name="", field_space="",
iteration_space="", index_offset=""):
# pylint: disable=too-many-arguments,arguments-renamed
'''
Create a new instance of a GOLoop with the expected children to
represent the bounds given by the loop properties.
:param parent: parent node of this GOLoop.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:param str loop_type: loop type - must be 'inner' or 'outer'.
:param str field_name: name of the field this loop iterates on.
:param str field_space: space of the field this loop iterates on.
:param str iteration_space: iteration space of the loop.
:param str index_offset: the grid index offset used by the kernel(s) \
within this loop.
:returns: a new GOLoop node (with appropriate child nodes).
:rtype: :py:class:`psyclone.gocean1p0.GOLoop`
'''
# Create loop node
node = GOLoop(parent, loop_type, field_name, field_space,
iteration_space, index_offset)
# Add start, stop, step and body and Loop children
node.addchild(node.lower_bound())
node.addchild(node.upper_bound())
node.addchild(Literal("1", INTEGER_TYPE))
node.addchild(Schedule())
return node
@property
def field_space(self):
'''
:returns: the loop's field space (e.g. CU, CV...).
:rtype: str
'''
return self._field_space
@field_space.setter
def field_space(self, my_field_space):
''' Sets new value for the field_space and updates the Loop bounds,
if these exist, to match the given field_space.
:param str my_field_space: new field_space value.
:raises TypeError: if the provided field_space is not a string.
:raises ValueError: if the provided field_space is not a valid GOcean \
field_space.
'''
# TODO 1393: This could call the super setter if the validations are
# generic
if not isinstance(my_field_space, str):
raise TypeError(
f"Field space must be a 'str' but found "
f"'{type(my_field_space).__name__}' instead.")
valid_fs = GOceanConstants().VALID_FIELD_GRID_TYPES + ['']
if my_field_space not in valid_fs:
raise ValueError(
f"Invalid string '{my_field_space}' provided for a GOcean "
f"field_space. The valid values are {valid_fs}")
self._field_space = my_field_space
if len(self.children) > 1:
self.start_expr.replace_with(self.lower_bound())
if len(self.children) > 2:
self.stop_expr.replace_with(self.upper_bound())
@property
def iteration_space(self):
'''
:returns: the loop's iteration space (e.g. 'go_internal_pts', \
'go_all_pts', ...).
:rtype: str
'''
return self._iteration_space
@iteration_space.setter
def iteration_space(self, it_space):
''' Sets new value for the iteration_space and updates the Loop bounds,
if these exist, to match the given iteration_space.
:param str it_space: new iteration_space value.
:raises TypeError: if the provided it_space is not a string.
'''
if not isinstance(it_space, str):
raise TypeError(
f"Iteration space must be a 'str' but found "
f"'{type(it_space).__name__}' instead.")
# TODO 1393: We could validate also the value, but at the moment there
# are some ambiguities to resolve.
self._iteration_space = it_space
if len(self.children) > 1:
self.start_expr.replace_with(self.lower_bound())
if len(self.children) > 2:
self.stop_expr.replace_with(self.upper_bound())
@property
def bounds_lookup(self):
'''
:returns: the GOcean loop bounds lookup table. This is a \
5-dimensional dictionary with index-offset, field-space, \
iteration-space, loop-type, and boundary-side lookup keys \
which provides information about how to construct the \
loop boundaries for a kernel with such parameters.
:rtype: dict
'''
return self._bounds_lookup
[docs]
def independent_iterations(self,
test_all_variables=False,
signatures_to_ignore=None,
dep_tools=None):
'''
This function is a GOcean-specific override of the default method
in the Loop class. It allows domain-specific rules to be applied when
determining whether or not loop iterations are independent.
:param bool test_all_variables: if True, it will test if all variable
accesses are independent, otherwise it will stop after the first
variable access is found that isn't.
:param signatures_to_ignore: list of signatures for which to skip
the access checks.
:type signatures_to_ignore: Optional[
List[:py:class:`psyclone.core.Signature`]]
:param dep_tools: an optional instance of DependencyTools so that the
caller can access any diagnostic messages detailing why the loop
iterations are not independent.
:type dep_tools: Optional[
:py:class:`psyclone.psyir.tools.DependencyTools`]
:returns: True if the loop iterations are independent, False otherwise.
:rtype: bool
'''
if not dep_tools:
dtools = DependencyTools()
else:
dtools = dep_tools
try:
stat = dtools.can_loop_be_parallelised(
self, test_all_variables=test_all_variables,
signatures_to_ignore=signatures_to_ignore)
return stat
except InternalError:
# The dependence analysis in GOcean doesn't yet use PSyIR
# consistently and that causes failures - TODO #845.
return True
# -------------------------------------------------------------------------
def _halo_read_access(self, arg):
'''Determines whether the supplied argument has (or might have) its
halo data read within this loop. Returns True if it does, or if
it might and False if it definitely does not.
:param arg: an argument contained within this loop.
:type arg: :py:class:`psyclone.gocean1p0.GOKernelArgument`
:return: True if the argument reads, or might read from the \
halo and False otherwise.
:rtype: bool
'''
return arg.argument_type == 'field' and arg.stencil.has_stencil and \
arg.access in [AccessType.READ, AccessType.READWRITE,
AccessType.INC]
[docs]
def create_halo_exchanges(self):
'''Add halo exchanges before this loop as required by fields within
this loop. The PSyIR insertion logic is coded in the _add_halo_exchange
helper method. '''
for halo_field in self.unique_fields_with_halo_reads():
# for each unique field in this loop that has its halo
# read, find the previous update of this field.
prev_arg_list = halo_field.backward_write_dependencies()
if not prev_arg_list:
# field has no previous dependence so create new halo
# exchange(s) as we don't know the state of the fields
# halo on entry to the invoke
# TODO 856: If dl_es_inf supported an is_dirty flag, we could
# be more selective on which HaloEx are needed. This
# function then will be the same as in LFRic and therefore
# the code can maybe be generalised.
self._add_halo_exchange(halo_field)
else:
prev_node = prev_arg_list[0].call
if not isinstance(prev_node, HaloExchange):
# Previous dependence is not a halo exchange so one needs
# to be added to satisfy the dependency in distributed
# memory.
self._add_halo_exchange(halo_field)
def _add_halo_exchange(self, halo_field):
'''An internal helper method to add the halo exchange call immediately
before this loop using the halo_field argument for the associated
field information.
:param halo_field: the argument requiring a halo exchange
:type halo_field: :py:class:`psyclone.gocean1p0.GOKernelArgument`
'''
exchange = GOHaloExchange(halo_field, parent=self.parent)
self.parent.children.insert(self.position, exchange)
# -------------------------------------------------------------------------
[docs]
@staticmethod
def setup_bounds():
'''Populates the GOLoop._bounds_lookup dictionary. This is
used by PSyclone to look up the loop boundaries for each loop
it creates.
'''
const = GOceanConstants()
for grid_offset in const.SUPPORTED_OFFSETS:
GOLoop._bounds_lookup[grid_offset] = {}
for gridpt_type in const.VALID_FIELD_GRID_TYPES:
GOLoop._bounds_lookup[grid_offset][gridpt_type] = {}
for itspace in const.VALID_ITERATES_OVER:
GOLoop._bounds_lookup[grid_offset][gridpt_type][
itspace] = {}
# Loop bounds for a mesh with NE offset
GOLoop._bounds_lookup['go_offset_ne']['go_ct']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_ne']['go_ct']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}"},
'outer': {'start': "{start}", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cu']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cu']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}-1"},
'outer': {'start': "{start}", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cv']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cv']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}"},
'outer': {'start': "{start}", 'stop': "{stop}-1"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cf']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}"},
'outer': {'start': "{start}-1", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_ne']['go_cf']['go_internal_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}-1"},
'outer': {'start': "{start}-1", 'stop': "{stop}-1"}}
# Loop bounds for a mesh with SE offset
GOLoop._bounds_lookup['go_offset_sw']['go_ct']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_sw']['go_ct']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}"},
'outer': {'start': "{start}", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cu']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cu']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}+1"},
'outer': {'start': "{start}", 'stop': "{stop}"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cv']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cv']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}"},
'outer': {'start': "{start}", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cf']['go_all_pts'] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
GOLoop._bounds_lookup['go_offset_sw']['go_cf']['go_internal_pts'] = \
{'inner': {'start': "{start}", 'stop': "{stop}+1"},
'outer': {'start': "{start}", 'stop': "{stop}+1"}}
# For offset 'any'
for gridpt_type in const.VALID_FIELD_GRID_TYPES:
for itspace in const.VALID_ITERATES_OVER:
GOLoop._bounds_lookup['go_offset_any'][gridpt_type][itspace] =\
{'inner': {'start': "{start}-1", 'stop': "{stop}"},
'outer': {'start': "{start}-1", 'stop': "{stop}"}}
# For 'every' grid-point type
for offset in const.SUPPORTED_OFFSETS:
for itspace in const.VALID_ITERATES_OVER:
GOLoop._bounds_lookup[offset]['go_every'][itspace] = \
{'inner': {'start': "{start}-1", 'stop': "{stop}+1"},
'outer': {'start': "{start}-1", 'stop': "{stop}+1"}}
# -------------------------------------------------------------------------
[docs]
@staticmethod
def add_bounds(bound_info):
# pylint: disable=too-many-locals
'''
Adds a new iteration space to PSyclone. An iteration space in the
gocean API is for a certain offset type and field type. It defines
the loop boundaries for the outer and inner loop. The format is a
":" separated tuple:
>>> bound_info = offset-type:field-type:iteration-space:outer-start:
outer-stop:inner-start:inner-stop
Example:
>>> bound_info = go_offset_ne:go_ct:go_all_pts:
{start}-1:{stop}+1:{start}:{stop}
The expressions {start} and {stop} will be replaced with the loop
indices that correspond to the inner points (i.e. non-halo or
boundary points) of the field. So the index {start}-1 is actually
on the halo / boundary.
:param str bound_info: A string that contains a ":" separated \
tuple with the iteration space definition.
:raises ValueError: if bound_info is not a string.
:raises ConfigurationError: if bound_info is not formatted correctly.
'''
if not isinstance(bound_info, str):
raise InternalError(f"The parameter 'bound_info' must be a "
f"string, got '{bound_info}' "
f"(type {type(bound_info)})")
data = bound_info.split(":")
if len(data) != 7:
raise ConfigurationError(f"An iteration space must be in the form "
f"\"offset-type:field-type:"
f"iteration-space:outer-start:"
f"outer-stop:inner-start:inner-stop\"\n"
f"But got \"{bound_info}\"")
if not GOLoop._bounds_lookup:
GOLoop.setup_bounds()
# Check that all bound specifications (min and max index) are valid.
# ------------------------------------------------------------------
# Regular expression that finds stings surrounded by {}
bracket_regex = re.compile("{[^}]+}")
for bound in data[3:7]:
all_expr = bracket_regex.findall(bound)
for bracket_expr in all_expr:
if bracket_expr not in ["{start}", "{stop}"]:
raise ConfigurationError(f"Only '{{start}}' and "
f"'{{stop}}' are allowed as "
f"bracketed expression in an "
f"iteration space. But got "
f"{bracket_expr}")
# We need to make sure the fparser is properly initialised, which
# typically has not yet happened when the config file is read.
# Otherwise the Nonlabel_Do_Stmt cannot parse valid expressions.
ParserFactory().create(std="f2008")
# Test both the outer loop indices (index 3 and 4) and inner
# indices (index 5 and 6):
for bound in data[3:7]:
do_string = f"do i=1, {bound}"
# Now replace any {start}/{stop} expression in the loop
# with a valid integer value:
do_string = do_string.format(start='15', stop='25')
# Check if the do loop can be parsed as a nonlabel do loop
try:
_ = Nonlabel_Do_Stmt(do_string)
except NoMatchError as err:
raise ConfigurationError(f"Expression '{bound}' is not a "
f"valid do loop boundary. Error "
f"message: '{err}'.") from err
# All tests successful, so add the new bounds:
# --------------------------------------------
current_bounds = GOLoop._bounds_lookup # Shortcut
# Check offset-type exists
if not data[0] in current_bounds:
current_bounds[data[0]] = {}
# Check field-type exists
if not data[1] in current_bounds[data[0]]:
current_bounds[data[0]][data[1]] = {}
const = GOceanConstants()
# Check iteration space exists:
if not data[2] in current_bounds[data[0]][data[1]]:
current_bounds[data[0]][data[1]][data[2]] = {}
const.VALID_ITERATES_OVER.append(data[2])
current_bounds[data[0]][data[1]][data[2]] = \
{'outer': {'start': data[3], 'stop': data[4]},
'inner': {'start': data[5], 'stop': data[6]}}
[docs]
def get_custom_bound_string(self, side):
'''
Get the string that represents a customized custom bound for this
GOLoop (provided by the add_bounds() method). It can provide the
'start' or 'stop' side of the bounds.
:param str side: 'start' or 'stop' side of the bound.
:returns: the string that represents the loop bound.
:rtype: str
:raises GenerationError: if this node can not find a field in \
the Invoke to be the base of the infrastructure call.
:raises GenerationError: if no expression is known to obtain the \
boundaries for a loop of this characteristics, because they \
are not in the GOcean lookup table or the loop type is not \
`inner` or `outer`.
'''
api_config = Config.get().api_conf("gocean")
# Get a field argument from the argument list
field = None
invoke = self.ancestor(InvokeSchedule)
if invoke:
for arg in invoke.symbol_table.argument_list:
if isinstance(arg.datatype, DataTypeSymbol):
if arg.datatype.name == "r2d_field":
field = arg
break
if field is None:
raise GenerationError(
f"Cannot generate custom loop bound for loop {self}. "
f"Couldn't find any suitable field.")
if self.loop_type == "inner":
prop_access = api_config.grid_properties["go_grid_xstop"]
elif self.loop_type == "outer":
prop_access = api_config.grid_properties["go_grid_ystop"]
else:
raise GenerationError(
f"Invalid loop type of '{self.loop_type}'. Expected one of "
f"{GOceanConstants().VALID_LOOP_TYPES}")
stop_expr = prop_access.fortran.format(field.name)
try:
bound = self.bounds_lookup[self.index_offset][self.field_space][
self.iteration_space][self.loop_type][side].format(
start='2', stop=stop_expr)
except KeyError as err:
raise GenerationError(
f"Cannot generate custom loop bound for a loop with an index-"
f"offset of '{self.index_offset}', a field-space of "
f"'{self.field_space}', an iteration-space of "
f"'{self.iteration_space}' and a loop-type of "
f"'{self.loop_type}', for the side '{side}' because "
f"this keys combination does not exist in the "
f"GOLoop.bounds_lookup table.") from err
return bound
# -------------------------------------------------------------------------
def _grid_property_psyir_expression(self, grid_property):
'''
Create a PSyIR reference expression using the supplied grid-property
information (which will have been read from the config file).
:param str grid_property: the property of the grid for which to \
create a reference. This is the format string read from the \
config file or just a simple name.
:returns: the PSyIR expression for the grid-property access.
:rtype: :py:class:`psyclone.psyir.nodes.Reference` or sub-class
'''
members = grid_property.split("%")
if len(members) == 1:
# We don't have a derived-type reference so create a Reference to
# a data symbol.
try:
sym = self.scope.symbol_table.lookup(members[0])
except KeyError:
sym = self.scope.symbol_table.new_symbol(
members[0], symbol_type=DataSymbol, datatype=INTEGER_TYPE)
return Reference(sym, parent=self)
if members[0] != "{0}":
raise NotImplementedError(
f"Supplied grid property is a derived-type reference but "
f"does not begin with '{{0}}': '{grid_property}'")
fld_sym = self.scope.symbol_table.lookup(self.field_name)
return StructureReference.create(fld_sym, members[1:])
[docs]
def upper_bound(self):
''' Creates the PSyIR of the upper bound of this loop.
:returns: the PSyIR for the upper bound of this loop.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
if self.field_space == "go_every":
# Bounds are independent of the grid-offset convention in use
# We look-up the upper bounds by enquiring about the SIZE of
# the array itself
stop = IntrinsicCall(IntrinsicCall.Intrinsic.SIZE)
# Use the data property to access the member of the field that
# contains the actual grid points.
api_config = Config.get().api_conf("gocean")
sref = self._grid_property_psyir_expression(
api_config.grid_properties["go_grid_data"].fortran)
stop.addchild(sref)
if self._loop_type == "inner":
stop.addchild(Literal("1", INTEGER_TYPE, parent=stop))
elif self._loop_type == "outer":
stop.addchild(Literal("2", INTEGER_TYPE, parent=stop))
return stop
# Loop bounds are pulled from a infrastructure call from a field
# object. For 'go_internal_pts' and 'go_all_points' we use the
# 'internal' and 'whole' structures respectively. For other
# iteration_spaces we look if a custom expression is defined in the
# lookup table.
props = Config.get().api_conf("gocean").grid_properties
if self.iteration_space.lower() == "go_internal_pts":
return self._grid_property_psyir_expression(
props[f"go_grid_internal_{self._loop_type}_stop"].fortran)
if self.iteration_space.lower() == "go_all_pts":
return self._grid_property_psyir_expression(
props[f"go_grid_whole_{self._loop_type}_stop"].fortran)
bound_str = self.get_custom_bound_string("stop")
return FortranReader().psyir_from_expression(
bound_str, self.ancestor(GOInvokeSchedule).symbol_table)
[docs]
def lower_bound(self):
''' Returns the lower bound of this loop as a string.
:returns: root of PSyIR sub-tree describing this lower bound.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
if self.field_space == "go_every":
# Bounds are independent of the grid-offset convention in use
return Literal("1", INTEGER_TYPE)
# Loop bounds are pulled from a infrastructure call from a field
# object. For 'go_internal_pts' and 'go_all_points' we use the
# 'internal' and 'whole' structures respectively. For other
# iteration_spaces we look if a custom expression is defined in the
# lookup table.
props = Config.get().api_conf("gocean").grid_properties
if self.iteration_space.lower() == "go_internal_pts":
return self._grid_property_psyir_expression(
props[f"go_grid_internal_{self._loop_type}_start"].fortran)
if self.iteration_space.lower() == "go_all_pts":
return self._grid_property_psyir_expression(
props[f"go_grid_whole_{self._loop_type}_start"].fortran)
bound_str = self.get_custom_bound_string("start")
return FortranReader().psyir_from_expression(
bound_str, self.ancestor(GOInvokeSchedule).symbol_table)
def _validate_loop(self):
''' Validate that the GOLoop has all necessary boundaries information
to lower to language-level PSyIR.
:raises GenerationError: if we can't find an enclosing Schedule.
:raises GenerationError: if this loop does not enclose a Kernel.
:raises GenerationError: if constant loop bounds are enabled but are \
not supported for the current grid offset.
:raises GenerationError: if the kernels within this loop expect \
different different grid offsets.
'''
# Our schedule holds the names to use for the loop bounds.
# Climb up the tree looking for our enclosing GOInvokeSchedule
schedule = self.ancestor(GOInvokeSchedule)
if schedule is None:
raise GenerationError("Cannot find a GOInvokeSchedule ancestor "
"for this GOLoop.")
# Walk down the tree looking for a kernel so that we can
# look-up what index-offset convention we are to use
go_kernels = self.walk(GOKern)
if not go_kernels:
raise GenerationError("Cannot find the "
"GOcean Kernel enclosed by this loop")
index_offset = go_kernels[0].index_offset
# Check that all kernels enclosed by this loop expect the same
# grid offset
for kernel in go_kernels:
if kernel.index_offset != index_offset:
raise GenerationError(f"All Kernels must expect the same "
f"grid offset but kernel "
f"'{kernel.name}' has offset"
f" '{kernel.index_offset}' which does "
f"not match '{index_offset}'.")
# pylint: disable=too-few-public-methods
[docs]
class GOBuiltInCallFactory():
''' A GOcean-specific built-in call factory. No built-ins
are supported in GOcean at the moment. '''
[docs]
@staticmethod
def create():
''' Placeholder to create a GOocean-specific built-in call.
This will require us to create a doubly-nested loop and then create
the body of the particular built-in operation. '''
raise GenerationError(
"Built-ins are not supported for the GOcean 1.0 API")
# pylint: disable=too-few-public-methods
[docs]
class GOKernCallFactory():
''' A GOcean-specific kernel-call factory. A standard kernel call in
GOcean consists of a doubly-nested loop (over i and j) and a call to
the user-supplied kernel routine. '''
[docs]
@staticmethod
def create(call, parent=None):
''' Create a new instance of a call to a GO kernel. Includes the
looping structure as well as the call to the kernel itself.
:param parent: node where the kernel call structure will be inserted.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
:returns: new PSyIR tree representing the kernel call loop.
:rtype: :py:class:`psyclone.gocean1p0.GOLoop`
'''
# Add temporary parent as the GOKern constructor needs to find its
# way to the top-level InvokeSchedule but we still don't have the
# PSyIR loops to place it in the appropriate place. We can't create
# the loops first because those depend on information provided by
# this kernel.
gocall = GOKern(call, parent=parent)
# Determine Loop information from the enclosed Kernel
iteration_space = gocall.iterates_over
field_space = gocall.arguments.iteration_space_arg().function_space
field_name = gocall.arguments.iteration_space_arg().name
index_offset = gocall.index_offset
# Create the double loop structure
outer_loop = GOLoop.create(loop_type="outer",
iteration_space=iteration_space,
field_space=field_space,
field_name=field_name,
index_offset=index_offset,
parent=parent)
inner_loop = GOLoop.create(loop_type="inner",
iteration_space=iteration_space,
field_space=field_space,
field_name=field_name,
index_offset=index_offset,
parent=outer_loop.loop_body)
outer_loop.loop_body.addchild(inner_loop)
# Remove temporary parent
# pylint: disable=protected-access
gocall._parent = None
inner_loop.loop_body.addchild(gocall)
return outer_loop
[docs]
class GOKern(CodedKern):
'''
Stores information about GOcean Kernels as specified by the Kernel
metadata. Uses this information to generate appropriate PSy layer
code for the Kernel instance.
:param call: information on the way in which this kernel is called
from the Algorithm layer.
:type call: :py:class:`psyclone.parse.algorithm.KernelCall`
:param parent: optional node where the kernel call will be inserted.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
'''
def __init__(self, call, parent=None):
super().__init__(GOKernelArguments, call, parent, check=False)
# Store the name of this kernel type (i.e. the name of the
# Fortran derived type containing its metadata).
self._metadata_name = call.ktype.name
# Pull out the grid index-offset that this kernel expects and
# store it here. This is used to check that all of the kernels
# invoked by an application are using compatible index offsets.
self._index_offset = call.ktype.index_offset
@staticmethod
def _create_psyir_for_access(symbol, var_value, depth):
'''This function creates the PSyIR of an index-expression:
- if `var_value` is negative, it returns 'symbol-depth'.
- if `var_value` is positive, it returns 'symbol+depth`
- otherwise it just returns a Reference to `symbol`.
This is used to create artificial stencil accesses for GOKernels.
:param symbol: the symbol to use.
:type symbol: :py:class:`psyclone.psyir.symbols.Symbol`
:param int var_value: value of the variable, which determines the \
direction (adding or subtracting depth).
:param int depth: the depth of the access (>0).
:returns: the index expression for an access in the given direction.
:rtype: union[:py:class:`psyclone.psyir.nodes.Reference`,
:py:class:`psyclone.psyir.nodes.BinaryOperation`]
'''
if var_value == 0:
return Reference(symbol)
if var_value > 0:
operator = BinaryOperation.Operator.ADD
else:
operator = BinaryOperation.Operator.SUB
return BinaryOperation.create(operator,
Reference(symbol),
Literal(str(depth), INTEGER_TYPE))
def _record_stencil_accesses(self, signature, arg, var_accesses):
'''This function adds accesses to a field depending on the
meta-data declaration for this argument (i.e. accounting for
any stencil accesses).
:param signature: signature of the variable.
:type signature: :py:class:`psyclone.core.Signature`
:param arg: the meta-data information for this argument.
:type arg: :py:class:`psyclone.gocean1p0.GOKernelArgument`
:param var_accesses: VariablesAccessMap instance that stores the\
information about the field accesses.
:type var_accesses: \
:py:class:`psyclone.core.VariablesAccessMap`
'''
# TODO #2530: if we parse the actual kernel code, it might not
# be required anymore to add these artificial accesses, instead
# the actual kernel accesses could be added.
sym_tab = self.ancestor(GOInvokeSchedule).symbol_table
symbol_i = sym_tab.lookup_with_tag("contiguous_kidx")
symbol_j = sym_tab.lookup_with_tag("noncontiguous_kidx")
# Query each possible stencil direction and add corresponding
# variable accesses. Note that if (i,j) itself is accessed, the
# depth will be 1, so one access to (i,j) is then added.
for j in [-1, 0, 1]:
for i in [-1, 0, 1]:
depth = arg.stencil.depth(i, j)
for current_depth in range(1, depth+1):
# Create PSyIR expressions for the required
# i+/- and j+/- expressions
i_expr = GOKern._create_psyir_for_access(symbol_i, i,
current_depth)
j_expr = GOKern._create_psyir_for_access(symbol_j, j,
current_depth)
# Even if a GOKern argument is declared to be written, it
# can only ever write to (i,j), so any other references
# must be read:
if i == 0 and j == 0:
acc = arg.access
else:
acc = AccessType.READ
var_accesses.add_access(signature, acc, self,
[i_expr, j_expr])
[docs]
def reference_accesses(self) -> VariablesAccessMap:
'''
:returns: a map of all the symbol accessed inside this node, the
keys are Signatures (unique identifiers to a symbol and its
structure acccessors) and the values are AccessSequence
(a sequence of AccessTypes).
'''
var_accesses = VariablesAccessMap()
# Grid properties are accessed using one of the fields. This stores
# the field used to avoid repeatedly determining the best field:
field_for_grid_property = None
for arg in self.arguments.args:
if arg.argument_type == "grid_property":
if not field_for_grid_property:
field_for_grid_property = \
self._arguments.find_grid_access()
var_name = arg.dereference(field_for_grid_property.name)
else:
var_name = arg.name
signature = Signature(var_name.split("%"))
if arg.is_scalar:
# The argument is only a variable if it is not a constant:
if not arg.is_literal:
var_accesses.add_access(signature, arg.access, self)
else:
if arg.argument_type == "field":
# Now add 'artificial' accesses to this field depending
# on meta-data (access-mode and stencil information):
self._record_stencil_accesses(signature, arg,
var_accesses)
else:
# In case of an array for now add an arbitrary array
# reference to (i,j) so it is properly recognised as
# an array access.
sym_tab = self.ancestor(GOInvokeSchedule).symbol_table
symbol_i = sym_tab.lookup_with_tag("contiguous_kidx")
symbol_j = sym_tab.lookup_with_tag("noncontiguous_kidx")
var_accesses.add_access(signature, arg.access,
self, [Reference(symbol_i),
Reference(symbol_j)])
var_accesses.update(super().reference_accesses())
return var_accesses
@property
def index_offset(self):
''' The grid index-offset convention that this kernel expects '''
return self._index_offset
[docs]
def get_callees(self):
'''
Obtains and returns the PSyIR Schedule representing the kernel code.
For consistency with LFRic kernels (which may be polymorphic), this
method actually returns a list comprising just one Schedule.
:returns: a schedule representing the GOcean kernel code.
:rtype: list[:py:class:`psyclone.gocean1p0.GOKernelSchedule`]
:raises GenerationError: if there is a problem raising the language-
level PSyIR of this kernel to GOcean PSyIR.
'''
if self._schedules:
return self._schedules
# Construct the PSyIR of the Fortran parse tree.
astp = Fparser2Reader()
psyir = astp.generate_psyir(self.ast)
# pylint: disable=import-outside-toplevel
from psyclone.domain.gocean.transformations import (
RaisePSyIR2GOceanKernTrans)
raise_trans = RaisePSyIR2GOceanKernTrans(self._metadata_name)
try:
raise_trans.apply(psyir)
except Exception as err:
raise GenerationError(
f"Failed to raise the PSyIR for kernel '{self.name}' "
f"to GOcean PSyIR. Error was:\n{err}") from err
for routine in psyir.walk(Routine):
if routine.name == self.name:
break
# We know the above loop will find the named routine because the
# previous raising transformation would have failed otherwise.
# pylint: disable=undefined-loop-variable
self._schedules = [routine]
return self._schedules
[docs]
class GOKernelArguments(Arguments):
'''Provides information about GOcean kernel-call arguments
collectively, as specified by the kernel argument metadata. This
class ensures that initialisation is performed correctly. It also
overrides the iteration_space_arg method to supply a
GOcean-specific dictionary for the mapping of argument-access
types.
:param call: the kernel meta-data for which to extract argument info.
:type call: :py:class:`psyclone.parse.KernelCall`
:param parent_call: the kernel-call object.
:type parent_call: :py:class:`psyclone.gocean1p0.GOKern`
:param bool check: whether to check for consistency between the \
kernel metadata and the algorithm layer. Defaults to \
True. Currently does nothing in this API.
'''
def __init__(self, call, parent_call, check=True):
# pylint: disable=unused-argument
Arguments.__init__(self, parent_call)
self._args = []
# Loop over the kernel arguments obtained from the meta data
for (idx, arg) in enumerate(call.ktype.arg_descriptors):
# arg is a GO1p0Descriptor object
if arg.argument_type == "grid_property":
# This is an argument supplied by the psy layer
self._args.append(GOKernelGridArgument(arg, parent_call))
elif arg.argument_type in ["scalar", "field"]:
# This is a kernel argument supplied by the Algorithm layer
self._args.append(GOKernelArgument(arg, call.args[idx],
parent_call))
else:
raise ParseError(f"Invalid kernel argument type. Found "
f"'{arg.argument_type}' but must be one of "
f"['grid_property', 'scalar', 'field'].")
self._dofs = []
[docs]
def psyir_expressions(self):
'''
:returns: the PSyIR expressions representing this Argument list.
:rtype: list of :py:class:`psyclone.psyir.nodes.Node`
'''
symtab = self._parent_call.scope.symbol_table
symbol1 = symtab.lookup_with_tag("contiguous_kidx")
symbol2 = symtab.lookup_with_tag("noncontiguous_kidx")
return ([Reference(symbol1), Reference(symbol2)] +
[arg.psyir_expression() for arg in self.args])
[docs]
def find_grid_access(self):
'''
Determine the best kernel argument from which to get properties of
the grid. For this, an argument must be a field (i.e. not
a scalar) and must be supplied by the algorithm layer
(i.e. not a grid property). If possible it should also be
a field that is read-only as otherwise compilers can get
confused about data dependencies and refuse to SIMD
vectorise.
:returns: the argument object from which to get grid properties.
:rtype: :py:class:`psyclone.gocean1p0.GOKernelArgument` or None
'''
for access in [AccessType.READ, AccessType.READWRITE,
AccessType.WRITE]:
for arg in self._args:
if arg.argument_type == "field" and arg.access == access:
return arg
# We failed to find any kernel argument which could be used
# to access the grid properties. This will only be a problem
# if the kernel requires a grid-property argument.
return None
@property
def dofs(self):
''' Currently required for invoke base class although this makes no
sense for GOcean. Need to refactor the Invoke base class and
remove the need for this property (#279). '''
return self._dofs
@property
def acc_args(self):
'''
Provide the list of references (both objects and arrays) that must
be present on an OpenACC device before the kernel associated with
this Arguments object may be launched.
:returns: list of (Fortran) quantities
:rtype: list of str
'''
arg_list = []
# First off, specify the field object which we will de-reference in
# order to get any grid properties (if this kernel requires them).
# We do this as some compilers do less optimisation if we get (read-
# -only) grid properties from a field object that has read-write
# access.
grid_fld = self.find_grid_access()
grid_ptr = grid_fld.name + "%grid"
api_config = Config.get().api_conf("gocean")
# TODO: #676 go_grid_data is actually a field property
data_fmt = api_config.grid_properties["go_grid_data"].fortran
arg_list.extend([grid_fld.name, data_fmt.format(grid_fld.name)])
for arg in self._args:
if arg.argument_type == "scalar":
arg_list.append(arg.name)
elif arg.argument_type == "field" and arg != grid_fld:
# The remote device will need the reference to the field
# object *and* the reference to the array within that object.
arg_list.extend([arg.name, data_fmt.format(arg.name)])
elif arg.argument_type == "grid_property":
if grid_ptr not in arg_list:
# This kernel needs a grid property and therefore the
# pointer to the grid object must be copied to the device.
arg_list.append(grid_ptr)
arg_list.append(grid_ptr+"%"+arg.name)
return arg_list
@property
def fields(self):
'''
Provides the list of names of field objects that are required by
the kernel associated with this Arguments object.
:returns: List of names of (Fortran) field objects.
:rtype: list of str
'''
args = args_filter(self._args, arg_types=["field"])
return [arg.name for arg in args]
@property
def scalars(self):
'''
Provides the list of names of scalar arguments required by the
kernel associated with this Arguments object. If there are none
then the returned list is empty.
:returns: A list of the names of scalar arguments in this object.
:rtype: list of str
'''
args = args_filter(self._args, arg_types=["scalar"])
return [arg.name for arg in args]
[docs]
def append(self, name, argument_type):
''' Create and append a GOKernelArgument to the Argument list.
:param str name: name of the appended argument.
:param str argument_type: type of the appended argument.
:raises TypeError: if the given name is not a string.
'''
if not isinstance(name, str):
raise TypeError(
f"The name parameter given to GOKernelArguments.append "
f"method should be a string, but found "
f"'{type(name).__name__}' instead.")
# Create a descriptor with the given type. `len(self.args)` gives the
# position in the argument list of the argument to which this
# descriptor corresponds. (This argument is appended in the code
# below.)
descriptor = Descriptor(None, argument_type, len(self.args))
# Create the argument and append it to the argument list
arg = Arg("variable", name)
argument = GOKernelArgument(descriptor, arg, self._parent_call)
self.args.append(argument)
[docs]
class GOKernelArgument(KernelArgument):
''' Provides information about individual GOcean kernel call arguments
as specified by the kernel argument metadata. '''
def __init__(self, arg, arg_info, call):
self._arg = arg
KernelArgument.__init__(self, arg, arg_info, call)
# Complete the argument initialisation as in some APIs it
# needs to be separated.
self._complete_init(arg_info)
[docs]
def psyir_expression(self):
'''
:returns: the PSyIR expression represented by this Argument.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
:raises InternalError: if this Argument type is not "field" or \
"scalar".
'''
# If the argument name is just a number (e.g. '0') we return a
# constant Literal expression
if self.name.isnumeric():
return Literal(self.name, INTEGER_TYPE)
# Now try for a real value. The constructor will raise an exception
# if the string is not a valid floating point number.
try:
return Literal(self.name, REAL_TYPE)
except ValueError:
pass
# Otherwise it's some form of Reference
symbol = self._call.scope.symbol_table.lookup(self.name)
# Gocean field arguments are StructureReferences to the %data attribute
if self.argument_type == "field":
return StructureReference.create(symbol, ["data"])
# Gocean scalar arguments are References to the variable
if self.argument_type == "scalar":
return Reference(symbol)
raise InternalError(f"GOcean expects the Argument.argument_type() to "
f"be 'field' or 'scalar' but found "
f"'{self.argument_type}'.")
[docs]
def infer_datatype(self):
''' Infer the datatype of this argument using the API rules.
:returns: the datatype of this argument.
:rtype: :py:class::`psyclone.psyir.symbols.DataType`
:raises InternalError: if this Argument type is not "field" or \
"scalar".
:raises InternalError: if this argument is scalar but its space \
property is not 'go_r_scalar' or 'go_i_scalar'.
'''
scope = self._call.ancestor(Container)
if scope is None:
# Prefer the module scope, but some tests that are disconnected can
# use the current scope
scope = self._call.scope
symtab = scope.symbol_table
# All GOcean fields are r2d_field
if self.argument_type == "field":
# r2d_field can have UnresolvedType and UnresolvedInterface because
# it is an unnamed import from a module.
type_symbol = symtab.find_or_create_tag(
"r2d_field", symbol_type=DataTypeSymbol,
datatype=UnresolvedType(), interface=UnresolvedInterface())
return type_symbol
# Gocean scalars can be REAL or INTEGER
if self.argument_type == "scalar":
if self.space.lower() == "go_r_scalar":
csym = symtab.find_or_create("kind_params_mod",
symbol_type=ContainerSymbol)
go_wp = symtab.find_or_create_tag(
"go_wp", symbol_type=DataSymbol, datatype=UnresolvedType(),
is_constant=True, interface=ImportInterface(csym))
return ScalarType(ScalarType.Intrinsic.REAL, Reference(go_wp))
if self.space.lower() == "go_i_scalar":
return INTEGER_TYPE
raise InternalError(f"GOcean expects scalar arguments to be of "
f"'go_r_scalar' or 'go_i_scalar' type but "
f"found '{self.space.lower()}'.")
raise InternalError(f"GOcean expects the Argument.argument_type() "
f"to be 'field' or 'scalar' but found "
f"'{self.argument_type}'.")
@property
def intrinsic_type(self):
'''
:returns: the intrinsic type of this argument. If it's not a scalar \
integer or real it will return an empty string.
:rtype: str
'''
if self.argument_type == "scalar":
if self.space.lower() == "go_r_scalar":
return "real"
if self.space.lower() == "go_i_scalar":
return "integer"
return ""
@property
def argument_type(self):
'''
Return the type of this kernel argument - whether it is a field,
a scalar or a grid_property (to be supplied by the PSy layer).
If it has no type it defaults to scalar.
:returns: the type of the argument.
:rtype: str
'''
if self._arg.argument_type:
return self._arg.argument_type
return "scalar"
@property
def function_space(self):
''' Returns the expected finite difference space for this
argument as specified by the kernel argument metadata.'''
return self._arg.function_space
@property
def is_scalar(self):
''':return: whether this variable is a scalar variable or not.
:rtype: bool'''
return self.argument_type == "scalar"
[docs]
class GOKernelGridArgument(Argument):
'''
Describes arguments that supply grid properties to a kernel.
These arguments are provided by the PSy layer rather than in
the Algorithm layer.
:param arg: the meta-data entry describing the required grid property.
:type arg: :py:class:`psyclone.gocean1p0.GO1p0Descriptor`
:param kernel_call: the kernel call node that this Argument belongs to.
:type kernel_call: :py:class:`psyclone.gocean1p0.GOKern`
:raises GenerationError: if the grid property is not recognised.
'''
def __init__(self, arg, kernel_call):
super().__init__(None, None, arg.access)
# Complete the argument initialisation as in some APIs it
# needs to be separated.
self._complete_init(None)
api_config = Config.get().api_conf("gocean")
try:
deref_name = api_config.grid_properties[arg.grid_prop].fortran
except KeyError as err:
all_keys = str(api_config.grid_properties.keys())
raise GenerationError(f"Unrecognised grid property specified. "
f"Expected one of {all_keys} but found "
f"'{arg.grid_prop}'") from err
# Each entry is a pair (name, type). Name can be subdomain%internal...
# so only take the last part after the last % as name.
self._name = deref_name.split("%")[-1]
# Store the original property name for easy lookup in is_scalar
self._property_name = arg.grid_prop
# This object always represents an argument that is a grid_property
self._argument_type = "grid_property"
# Reference to the Call this argument belongs to
self._call = kernel_call
@property
def name(self):
''' Returns the Fortran name of the grid property, which is used
in error messages etc.'''
return self._name
[docs]
def psyir_expression(self):
'''
:returns: the PSyIR expression represented by this Argument.
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
# Find field from which to access grid properties
base_field = self._call.arguments.find_grid_access().name
tag = "AlgArgs_" + base_field
symbol = self._call.scope.symbol_table.find_or_create_tag(tag)
# Get aggregate grid type accessors without the base name
access = self.dereference(base_field).split('%')[1:]
# Construct the PSyIR reference
return StructureReference.create(symbol, access)
[docs]
def dereference(self, fld_name):
'''Returns a Fortran string to dereference a grid property of the
specified field. It queries the current config file settings for
getting the proper dereference string, which is a format string
where {0} represents the field name.
:param str fld_name: The name of the field which is used to \
dereference a grid property.
:returns: the dereference string required to access a grid property
in a dl_esm field (e.g. "subdomain%internal%xstart"). The name
must contains a "{0}" which is replaced by the field name.
:rtype: str'''
api_config = Config.get().api_conf("gocean")
deref_name = api_config.grid_properties[self._property_name].fortran
return deref_name.format(fld_name)
@property
def argument_type(self):
''' The type of this argument. We have this for compatibility with
GOKernelArgument objects since, for this class, it will always be
"grid_property". '''
return self._argument_type
@property
def intrinsic_type(self):
'''
:returns: the intrinsic_type of this argument.
:rtype: str
'''
api_config = Config.get().api_conf("gocean")
return api_config.grid_properties[self._property_name].intrinsic_type
@property
def is_scalar(self):
'''
:returns: if this variable is a scalar variable or not.
:rtype: bool
'''
# The constructor guarantees that _property_name is a valid key!
api_config = Config.get().api_conf("gocean")
return api_config.grid_properties[self._property_name].type \
== "scalar"
@property
def text(self):
''' The raw text used to pass data from the algorithm layer
for this argument. Grid properties are not passed from the
algorithm layer so None is returned.'''
return None
[docs]
def forward_dependence(self):
'''
A grid-property argument is read-only and supplied by the
PSy layer so has no dependencies
:returns: None to indicate no dependencies
:rtype: NoneType
'''
return None
[docs]
def backward_dependence(self):
'''
A grid-property argument is read-only and supplied by the
PSy layer so has no dependencies
:returns: None to indicate no dependencies
:rtype: NoneType
'''
return None
[docs]
class GOStencil():
'''GOcean 1.0 stencil information for a kernel argument as obtained by
parsing the kernel meta-data. The expected structure of the
metadata and its meaning is provided in the description of the
load method
'''
def __init__(self):
''' Set up any internal variables. '''
self._has_stencil = None
self._stencil = [[0 for _ in range(3)] for _ in range(3)]
self._name = None
self._initialised = False
# pylint: disable=too-many-branches
[docs]
def load(self, stencil_info, kernel_name):
'''Take parsed stencil metadata information, check it is valid and
store it in a convenient form. The kernel_name argument is
only used to provide the location if there is an error.
The stencil information should either be a name which
indicates a particular type of stencil or in the form
stencil(xxx,yyy,zzz) which explicitly specifies a stencil
shape where xxx, yyy and zzz are triplets of integers
indicating whether there is a stencil access in a particular
direction and the depth of that access. For example:
go_stencil(010, ! N
212, ! W E
010) ! S
indicates that there is a stencil access of depth 1 in the
"North" and "South" directions and stencil access of depth 2
in the "East" and "West" directions. The value at the centre
of the stencil will not be used by PSyclone but can be 0 or 1
and indicates whether the local field value is accessed.
The convention is for the associated arrays to be
2-dimensional. If we denote the first dimension as "i" and the
second dimension as "j" then the following directions are
assumed:
> j
> ^
> |
> |
> ---->i
For example a stencil access like:
a(i,j) + a(i+1,j) + a(i,j-1)
would be stored as:
go_stencil(000,
011,
010)
:param stencil_info: contains the appropriate part of the parser AST
:type stencil_info: :py:class:`psyclone.expression.FunctionVar`
:param string kernel_name: the name of the kernel from where this \
stencil information came from.
:raises ParseError: if the supplied stencil information is invalid.
'''
self._initialised = True
if not isinstance(stencil_info, expr.FunctionVar):
# the stencil information is not in the expected format
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd descriptor "
f"(stencil) of field argument is '{stencil_info}' but "
f"expected either a name or the format 'go_stencil(...)'")
# Get the name
name = stencil_info.name.lower()
const = GOceanConstants()
if stencil_info.args:
# The stencil info is of the form 'name(a,b,...), so the
# name should be 'stencil' and there should be 3
# arguments'
self._has_stencil = True
args = stencil_info.args
if name != "go_stencil":
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument is '{name}' but "
f"must be 'go_stencil(...)")
if len(args) != 3:
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument with format "
f"'go_stencil(...)', has {len(args)} arguments but should "
f"have 3")
# Each of the 3 args should be of length 3 and each
# character should be a digit from 0-9. Whilst we are
# expecting numbers, the parser represents these numbers
# as strings so we have to perform string manipulation to
# check and that extract them
for arg_idx in range(3):
arg = args[arg_idx]
if not isinstance(arg, str):
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument with format "
f"'go_stencil(...)'. Argument index {arg_idx} should "
f"be a number but found '{arg}'.")
if len(arg) != 3:
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument with format "
f"'go_stencil(...)'. Argument index {arg_idx} should "
f"consist of 3 digits but found {len(arg)}.")
# The central value is constrained to be 0 or 1
if args[1][1] not in ["0", "1"]:
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument with format "
f"'go_stencil(...)'. Argument index 1 position 1 "
f"should be a number from 0-1 but found {args[1][1]}.")
# It is not valid to specify a zero stencil. This is
# indicated by the 'pointwise' name
if args[0] == "000" and \
(args[1] == "000" or args[1] == "010") and \
args[2] == "000":
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument with format "
f"'go_stencil(...)'. A zero sized stencil has been "
f"specified. This should be specified with the "
f"'go_pointwise' keyword.")
# store the values in an internal array as integers in i,j
# order
for idx0 in range(3):
for idx1 in range(3):
# The j coordinate needs to be 'reversed': the first
# row (index 0 in args) is 'top', which should be
# accessed using '+1', and the last row (index 2 in args)
# needs to be accessed using '-1' (see depth()). Using
# 2-idx1 mirrors the rows appropriately.
self._stencil[idx0][2-idx1] = int(args[idx1][idx0])
else:
# stencil info is of the form 'name' so should be one of
# our valid names
if name not in const.VALID_STENCIL_NAMES:
raise ParseError(
f"Meta-data error in kernel '{kernel_name}': 3rd "
f"descriptor (stencil) of field argument is '{name}' "
f"but must be one of {const.VALID_STENCIL_NAMES} or "
f"go_stencil(...)")
self._name = name
# We currently only support one valid name ('pointwise')
# which indicates that there is no stencil
self._has_stencil = False
# Define a 'stencil' for pointwise so that depth() can be used for
# pointwise kernels without handling pointwise as special case
self._stencil = [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
def _check_init(self):
'''Internal method which checks that the stencil information has been
loaded.
:raises GenerationError: if the GOStencil object has not been
initialised i.e. the load() method has not been called
'''
if not self._initialised:
raise GenerationError(
"Error in class GOStencil: the object has not yet been "
"initialised. Please ensure the load() method is called.")
@property
def has_stencil(self):
'''Specifies whether this argument has stencil information or not. The
only case when this is False is when the stencil information
specifies 'pointwise' as this indicates that there is no
stencil access.
:returns: True if this argument has stencil information and False \
if not.
:rtype: bool
'''
self._check_init()
return self._has_stencil
@property
def name(self):
'''Provides the stencil name if one is provided
:returns: the name of the type of stencil if this is provided \
and 'None' if not.
:rtype: str
'''
self._check_init()
return self._name
[docs]
def depth(self, index0, index1):
'''Provides the depth of the stencil in the 8 possible stencil
directions in a 2d regular grid (see the description in the
load class for more information). Values must be between -1
and 1 as they are considered to be relative to the centre of
the stencil For example:
stencil(234,
915,
876)
returns
depth(-1,0) = 9
depth(1,1) = 4
:param int index0: the relative stencil offset for the first \
index of the associated array. This value \
must be between -1 and 1.
:param int index1: the relative stencil offset for the second \
index of the associated array. This value \
must be between -1 and 1
:returns: the depth of the stencil in the specified direction.
:rtype: int
:raises GenerationError: if the indices are out-of-bounds.
'''
self._check_init()
if index0 < -1 or index0 > 1 or index1 < -1 or index1 > 1:
raise GenerationError(
f"The indices arguments to the depth method in the GOStencil "
f"object must be between -1 and 1 but found "
f"({index0},{index1})")
return self._stencil[index0+1][index1+1]
[docs]
class GO1p0Descriptor(Descriptor):
''' Description of a GOcean 1.0 kernel argument, as obtained by
parsing the kernel metadata.
:param str kernel_name: the name of the kernel metadata type \
that contains this metadata.
:param kernel_arg: the relevant part of the parser's AST.
:type kernel_arg: :py:class:`psyclone.expression.FunctionVar`
:param int metadata_index: the postion of this argument in the list of \
arguments specified in the metadata.
:raises ParseError: if a kernel argument has an invalid grid-point type.
:raises ParseError: for an unrecognised grid property.
:raises ParseError: for an invalid number of arguments.
:raises ParseError: for an invalid access argument.
'''
def __init__(self, kernel_name, kernel_arg, metadata_index):
# pylint: disable=too-many-locals
nargs = len(kernel_arg.args)
stencil_info = None
const = GOceanConstants()
if nargs == 3:
# This kernel argument is supplied by the Algorithm layer
# and is either a field or a scalar
access = kernel_arg.args[0].name
funcspace = kernel_arg.args[1].name
stencil_info = GOStencil()
stencil_info.load(kernel_arg.args[2],
kernel_name)
# Valid values for the grid-point type that a kernel argument
# may have. (We use the funcspace argument for this as it is
# similar to the space in Finite-Element world.)
valid_func_spaces = const.VALID_FIELD_GRID_TYPES + \
const.VALID_SCALAR_TYPES
self._grid_prop = ""
if funcspace.lower() in const.VALID_FIELD_GRID_TYPES:
self._argument_type = "field"
elif funcspace.lower() in const.VALID_SCALAR_TYPES:
self._argument_type = "scalar"
else:
raise ParseError(f"Meta-data error in kernel {kernel_name}: "
f"argument grid-point type is '{funcspace}' "
f"but must be one of {valid_func_spaces}")
elif nargs == 2:
# This kernel argument is a property of the grid. The grid
# properties are special because they must be supplied by
# the PSy layer.
access = kernel_arg.args[0].name
grid_var = kernel_arg.args[1].name
funcspace = ""
self._grid_prop = grid_var
self._argument_type = "grid_property"
api_config = Config.get().api_conf("gocean")
if grid_var.lower() not in api_config.grid_properties:
valid_keys = str(api_config.grid_properties.keys())
raise ParseError(
f"Meta-data error in kernel {kernel_name}: un-recognised "
f"grid property '{grid_var}' requested. Must be one of "
f"{valid_keys}")
else:
raise ParseError(
f"Meta-data error in kernel {kernel_name}: 'arg' type "
f"expects 2 or 3 arguments but found '{len(kernel_arg.args)}' "
f"in '{kernel_arg.args}'")
api_config = Config.get().api_conf("gocean")
access_mapping = api_config.get_access_mapping()
try:
access_type = access_mapping[access]
except KeyError as err:
valid_names = api_config.get_valid_accesses_api()
raise ParseError(
f"Meta-data error in kernel {kernel_name}: argument access is "
f"given as '{access}' but must be one of {valid_names}"
) from err
# Finally we can call the __init__ method of our base class
super().__init__(access_type, funcspace, metadata_index,
stencil=stencil_info,
argument_type=self._argument_type)
def __str__(self):
return repr(self)
@property
def grid_prop(self):
'''
:returns: the name of the grid-property that this argument is to \
supply to the kernel.
:rtype: str
'''
return self._grid_prop
[docs]
class GOKernelType1p0(KernelType):
''' Description of a kernel including the grid index-offset it
expects and the region of the grid that it expects to
operate upon '''
def __str__(self):
return ('GOcean 1.0 kernel ' + self._name + ', index-offset = ' +
self._index_offset + ', iterates-over = ' +
self._iterates_over)
def __init__(self, ast, name=None):
# Initialise the base class
KernelType.__init__(self, ast, name=name)
# What grid offset scheme this kernel expects
self._index_offset = self._ktype.get_variable('index_offset').init
const = GOceanConstants()
if self._index_offset is None:
raise ParseError(f"Meta-data error in kernel {name}: an "
f"INDEX_OFFSET must be specified and must be "
f"one of {const.VALID_OFFSET_NAMES}")
if self._index_offset.lower() not in const.VALID_OFFSET_NAMES:
raise ParseError(f"Meta-data error in kernel {name}: "
f"INDEX_OFFSET has value '{self._index_offset}'"
f" but must be one of {const.VALID_OFFSET_NAMES}")
const = GOceanConstants()
# Check that the meta-data for this kernel is valid
if self._iterates_over is None:
raise ParseError(f"Meta-data error in kernel {name}: "
f"ITERATES_OVER is missing. (Valid values are: "
f"{const.VALID_ITERATES_OVER})")
if self._iterates_over.lower() not in const.VALID_ITERATES_OVER:
raise ParseError(f"Meta-data error in kernel {name}: "
f"ITERATES_OVER has value '"
f"{self._iterates_over.lower()}' but must be "
f"one of {const.VALID_ITERATES_OVER}")
# The list of kernel arguments
self._arg_descriptors = []
have_grid_prop = False
for idx, init in enumerate(self._inits):
if init.name != 'go_arg':
raise ParseError(f"Each meta_arg value must be of type "
f"'go_arg' for the gocean api, but "
f"found '{init.name}'")
# Pass in the name of this kernel for the purposes
# of error reporting
new_arg = GO1p0Descriptor(name, init, idx)
# Keep track of whether this kernel requires any
# grid properties
have_grid_prop = (have_grid_prop or
(new_arg.argument_type == "grid_property"))
self._arg_descriptors.append(new_arg)
# If this kernel expects a grid property then check that it
# has at least one field object as an argument (which we
# can use to access the grid)
if have_grid_prop:
have_fld = False
for arg in self.arg_descriptors:
if arg.argument_type == "field":
have_fld = True
break
if not have_fld:
raise ParseError(
f"Kernel {name} requires a property of the grid but does "
f"not have any field objects as arguments.")
# Override nargs from the base class so that it returns the no.
# of args specified in the algorithm layer (and thus excludes those
# that must be added in the PSy layer). This is done to simplify the
# check on the no. of arguments supplied in any invoke of the kernel.
@property
def nargs(self):
''' Count and return the number of arguments that this kernel
expects the Algorithm layer to provide '''
count = 0
for arg in self.arg_descriptors:
if arg.argument_type != "grid_property":
count += 1
return count
@property
def index_offset(self):
''' Return the grid index-offset that this kernel expects '''
return self._index_offset
[docs]
class GOACCEnterDataDirective(ACCEnterDataDirective):
'''
Sub-classes ACCEnterDataDirective to provide the dl_esm_inf infrastructure-
specific interfaces to flag and update when data is on a device.
'''
def _read_from_device_routine(self):
''' Return the symbol of the routine that reads data from the OpenACC
device, if it doesn't exist create the Routine and the Symbol.
:returns: the symbol representing the read_from_device routine.
:rtype: :py:class:`psyclone.psyir.symbols.symbol`
:raises GenerationError: if this class is lowered from a location where
a Routine can not be inserted.
'''
# Insert the routine as a child of the ancestor Container
if not self.ancestor(Container):
raise GenerationError(
f"The GOACCEnterDataDirective can only be generated/lowered "
f"inside a Container in order to insert a sibling "
f"subroutine, but '{self}' is not inside a Container.")
symtab = self.ancestor(Container).symbol_table
try:
return symtab.lookup_with_tag("openacc_read_func")
except KeyError:
# If the subroutines does not exist, it needs to be
# generated first.
pass
code = '''
subroutine read_openacc(from, to, startx, starty, nx, ny, blocking)
use iso_c_binding, only: c_ptr
use kind_params_mod, only: go_wp
type(c_ptr), intent(in) :: from
real(go_wp), dimension(:,:), intent(inout), target :: to
integer, intent(in) :: startx, starty, nx, ny
logical, intent(in) :: blocking
end subroutine read_openacc
'''
# Create the symbol for the routine and add it to the symbol table.
subroutine_symbol = RoutineSymbol("read_from_device")
# Obtain the PSyIR representation of the code above
fortran_reader = FortranReader()
container = fortran_reader.psyir_from_source(code)
subroutine = container.children[0]
# Add an ACCUpdateDirective inside the subroutine
subroutine.addchild(ACCUpdateDirective([Signature("to")], "host",
if_present=False))
self.ancestor(Container).symbol_table.add(subroutine_symbol,
tag="openacc_read_func")
subroutine.detach()
subroutine.symbol = subroutine_symbol
self.ancestor(Container).addchild(subroutine)
return symtab.lookup_with_tag("openacc_read_func")
[docs]
def lower_to_language_level(self):
'''
In-place replacement of DSL or high-level concepts into generic PSyIR
constructs. In addition to calling this method in the base class, the
GOACCEnterDataDirective sets up the 'data_on_device' flag for
each of the fields accessed.
:returns: the lowered version of this node.
:rtype: :py:class:`psyclone.psyir.node.Node`
'''
self._acc_dirs = self.ancestor(InvokeSchedule).walk(
(ACCParallelDirective, ACCKernelsDirective))
obj_list = []
for pdir in self._acc_dirs:
for var in pdir.fields:
if var not in obj_list:
obj_list.append(var)
read_routine_symbol = self._read_from_device_routine()
for var in obj_list:
symbol = self.scope.symbol_table.lookup(var)
assignment = Assignment.create(
StructureReference.create(symbol, ['data_on_device']),
Literal("true", BOOLEAN_TYPE))
self.parent.children.insert(self.position, assignment)
# Use a CodeBlock to encode a Fortran pointer assignment
reader = FortranReader()
codeblock = reader.psyir_from_statement(
f"{symbol.name}%read_from_device_f => "
f"{read_routine_symbol.name}\n",
self.scope.symbol_table)
self.parent.children.insert(self.position, codeblock)
return super().lower_to_language_level()
[docs]
class GOKernelSchedule(KernelSchedule):
'''
Sub-classes KernelSchedule to provide a GOcean-specific implementation.
:param str name: Kernel subroutine name
'''
# Polymorphic parameter to initialize the Symbol Table of the Schedule
_symbol_table_class = GOSymbolTable
[docs]
class GOHaloExchange(HaloExchange):
'''GOcean specific halo exchange class which can be added to and
manipulated in a schedule.
:param field: the field that this halo exchange will act on.
:type field: :py:class:`psyclone.gocean1p0.GOKernelArgument`
:param bool check_dirty: optional argument default False (contrary to \
its generic class - revisit in #856) indicating whether this halo \
exchange should be subject to a run-time check for clean/dirty halos.
:param parent: optional PSyIR parent node (default None) of this object.
:type parent: :py:class:`psyclone.psyir.nodes.Node`
'''
def __init__(self, field, check_dirty=False, parent=None):
super().__init__(field, check_dirty=check_dirty, parent=parent)
# Name of the HaloExchange method in the GOcean infrastructure.
self._halo_exchange_name = "halo_exchange"
[docs]
def lower_to_language_level(self):
'''
In-place replacement of DSL or high-level concepts into generic
PSyIR constructs. A GOHaloExchange is replaced by a call to the
appropriate library method.
:returns: the lowered version of this node.
:rtype: :py:class:`psyclone.psyir.node.Node`
'''
# TODO 856: Wrap Halo call with an is_dirty flag when necessary.
# TODO 886: Currently only stencils of depth 1 are accepted by this
# API, so the HaloExchange is hardcoded to depth 1.
# Call the halo_exchange routine with depth argument to 1
# Currently we create an symbol name with % as a workaround of not
# having type bound routines.
rsymbol = RoutineSymbol(self.field.name + "%" +
self._halo_exchange_name)
call_node = Call.create(rsymbol, [Literal("1", INTEGER_TYPE)])
self.replace_with(call_node)
return call_node
# For Sphinx AutoAPI documentation generation
__all__ = ['GOPSy', 'GOInvokes', 'GOInvoke', 'GOInvokeSchedule', 'GOLoop',
'GOBuiltInCallFactory', 'GOKernCallFactory', 'GOKern',
'GOKernelArguments', 'GOKernelArgument',
'GOKernelGridArgument', 'GOStencil', 'GO1p0Descriptor',
'GOKernelType1p0', 'GOACCEnterDataDirective',
'GOKernelSchedule', 'GOHaloExchange']