Reference Guide  2.5.0
psyGen.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2017-2024, Science and Technology Facilities Council.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice,
14 # this list of conditions and the following disclaimer in the documentation
15 # and/or other materials provided with the distribution.
16 #
17 # * Neither the name of the copyright holder nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 # POSSIBILITY OF SUCH DAMAGE.
33 # -----------------------------------------------------------------------------
34 # Authors: R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
35 # Modified by I. Kavcic and L. Turner, Met Office
36 # Modified by C.M. Maynard, Met Office / University of Reading
37 # Modified by J. Henrichs, Bureau of Meteorology
38 # -----------------------------------------------------------------------------
39 
40 ''' This module provides generic support for PSyclone's PSy code optimisation
41  and generation. The classes in this method need to be specialised for a
42  particular API and implementation. '''
43 
44 import os
45 from collections import OrderedDict
46 import abc
47 
48 from psyclone.configuration import Config
49 from psyclone.core import AccessType
50 from psyclone.errors import GenerationError, InternalError, FieldNotFoundError
51 from psyclone.f2pygen import (AllocateGen, AssignGen, CommentGen,
52  DeclGen, DeallocateGen, DoGen, UseGen)
53 from psyclone.parse.algorithm import BuiltInCall
54 from psyclone.psyir.backend.fortran import FortranWriter
55 from psyclone.psyir.nodes import (ArrayReference, Call, Container, Literal,
56  Loop, Node, OMPDoDirective, Reference,
57  Routine, Schedule, Statement)
58 from psyclone.psyir.symbols import (ArgumentInterface, ArrayType,
59  ContainerSymbol, DataSymbol,
60  UnresolvedType,
61  ImportInterface, INTEGER_TYPE,
62  RoutineSymbol, Symbol)
63 from psyclone.psyir.symbols.datatypes import UnsupportedFortranType
64 
65 # The types of 'intent' that an argument to a Fortran subroutine
66 # may have
67 FORTRAN_INTENT_NAMES = ["inout", "out", "in"]
68 
69 # Mapping of access type to operator.
70 REDUCTION_OPERATOR_MAPPING = {AccessType.SUM: "+"}
71 
72 
73 def object_index(alist, item):
74  '''
75  A version of the `list.index()` method that checks object identity
76  rather that the content of the object.
77 
78  TODO this is a workaround for the fact that fparser2 overrides the
79  comparison operator for all nodes in the parse tree. See fparser
80  issue 174.
81 
82  :param alist: single object or list of objects to search.
83  :type alist: list or :py:class:`fparser.two.utils.Base`
84  :param obj item: object to search for in the list.
85  :returns: index of the item in the list.
86  :rtype: int
87  :raises ValueError: if object is not in the list.
88  '''
89  if item is None:
90  raise InternalError("Cannot search for None item in list.")
91  for idx, entry in enumerate(alist):
92  if entry is item:
93  return idx
94  raise ValueError(f"Item '{item}' not found in list: {alist}")
95 
96 
97 def get_api(api):
98  ''' If no API is specified then return the default. Otherwise, check that
99  the supplied API is valid.
100  :param str api: The PSyclone API to check or an empty string.
101  :returns: The API that is in use.
102  :rtype: str
103  :raises GenerationError: if the specified API is not supported.
104 
105  '''
106  if api == "":
107  api = Config.get().default_api
108  else:
109  if api not in Config.get().supported_apis:
110  raise GenerationError(f"get_api: Unsupported API '{api}' "
111  f"specified. Supported types are "
112  f"{Config.get().supported_apis}.")
113  return api
114 
115 
116 def zero_reduction_variables(red_call_list, parent):
117  '''zero all reduction variables associated with the calls in the call
118  list'''
119  if red_call_list:
120  parent.add(CommentGen(parent, ""))
121  parent.add(CommentGen(parent, " Zero summation variables"))
122  parent.add(CommentGen(parent, ""))
123  for call in red_call_list:
124  call.zero_reduction_variable(parent)
125  parent.add(CommentGen(parent, ""))
126 
127 
128 def args_filter(arg_list, arg_types=None, arg_accesses=None, arg_meshes=None,
129  include_literals=True):
130  '''
131  Return all arguments in the supplied list that are of type
132  arg_types and with access in arg_accesses. If these are not set
133  then return all arguments.
134 
135  :param arg_list: list of kernel arguments to filter.
136  :type arg_list: list of :py:class:`psyclone.parse.kernel.Descriptor`
137  :param arg_types: list of argument types (e.g. "GH_FIELD").
138  :type arg_types: list of str
139  :param arg_accesses: list of access types that arguments must have.
140  :type arg_accesses: list of \
141  :py:class:`psyclone.core.access_type.AccessType`
142  :param arg_meshes: list of meshes that arguments must be on.
143  :type arg_meshes: list of str
144  :param bool include_literals: whether or not to include literal arguments \
145  in the returned list.
146 
147  :returns: list of kernel arguments matching the requirements.
148  :rtype: list of :py:class:`psyclone.parse.kernel.Descriptor`
149 
150  '''
151  arguments = []
152  for argument in arg_list:
153  if arg_types:
154  if argument.argument_type.lower() not in arg_types:
155  continue
156  if arg_accesses:
157  if argument.access not in arg_accesses:
158  continue
159  if arg_meshes:
160  if argument.mesh not in arg_meshes:
161  continue
162  if not include_literals:
163  # We're not including literal arguments so skip this argument
164  # if it is literal.
165  if argument.is_literal:
166  continue
167  arguments.append(argument)
168  return arguments
169 
170 
171 class PSyFactory():
172  '''
173  Creates a specific version of the PSy. If a particular api is not
174  provided then the default api, as specified in the psyclone.cfg
175  file, is chosen.
176 
177  :param str api: name of the PSyclone API (domain) for which to create \
178  a factory.
179  :param bool distributed_memory: whether or not the PSy object created \
180  will include support for distributed-memory parallelism.
181 
182  :raises TypeError: if the distributed_memory argument is not a bool.
183 
184  '''
185  def __init__(self, api="", distributed_memory=None):
186 
187  if distributed_memory is None:
188  _distributed_memory = Config.get().distributed_memory
189  else:
190  _distributed_memory = distributed_memory
191 
192  if _distributed_memory not in [True, False]:
193  raise TypeError(
194  "The distributed_memory flag in PSyFactory must be set to"
195  " 'True' or 'False'")
196  Config.get().distributed_memory = _distributed_memory
197  self._type_type = get_api(api)
198 
199  def create(self, invoke_info):
200  '''
201  Create the API-specific PSy instance.
202 
203  :param invoke_info: information on the invoke()s found by parsing \
204  the Algorithm layer or (for NEMO) the fparser2 \
205  parse tree of the source file.
206  :type invoke_info: :py:class:`psyclone.parse.algorithm.FileInfo` or \
207  :py:class:`fparser.two.Fortran2003.Program`
208 
209  :returns: an instance of the API-specific sub-class of PSy.
210  :rtype: subclass of :py:class:`psyclone.psyGen.PSy`
211 
212  :raises InternalError: if this factory is found to have an \
213  unsupported type (API).
214  '''
215  # Conditional run-time importing is a part of this factory
216  # implementation.
217  # pylint: disable=import-outside-toplevel
218  if self._type_type == "dynamo0.3":
219  from psyclone.dynamo0p3 import DynamoPSy as PSyClass
220  elif self._type_type == "gocean1.0":
221  from psyclone.gocean1p0 import GOPSy as PSyClass
222  elif self._type_type == "nemo":
223  from psyclone.nemo import NemoPSy as PSyClass
224  # For this API, the 'invoke_info' is actually the fparser2 AST
225  # of the Fortran file being processed
226  else:
227  raise InternalError(
228  f"PSyFactory: Unsupported API type '{self._type}' found. "
229  f"Expected one of {Config.get().supported_apis}.")
230  return PSyClass(invoke_info)
231 
232 
233 class PSy():
234  '''
235  Base class to help manage and generate PSy code for a single
236  algorithm file. Takes the invocation information output from the
237  function :func:`parse.algorithm.parse` as its input and stores this in a
238  way suitable for optimisation and code generation.
239 
240  :param FileInfo invoke_info: An object containing the required \
241  invocation information for code \
242  optimisation and generation. Produced \
243  by the function :func:`parse.algorithm.parse`.
244  :type invoke_info: :py:class:`psyclone.parse.algorithm.FileInfo`
245 
246  For example:
247 
248  >>> from psyclone.parse.algorithm import parse
249  >>> ast, info = parse("argspec.F90")
250  >>> from psyclone.psyGen import PSyFactory
251  >>> api = "..."
252  >>> psy = PSyFactory(api).create(info)
253  >>> print(psy.gen)
254 
255  '''
256  def __init__(self, invoke_info):
257  self._name_name = invoke_info.name
258  self._invokes_invokes = None
259  # create an empty PSy layer container
260  # TODO 1010: Alternatively the PSy object could be a Container itself
261  self._container_container = Container(self.namename)
262 
263  @property
264  def container(self):
265  '''
266  :returns: the container associated with this PSy object
267  :rtype: :py:class:`psyclone.psyir.nodes.Container`
268  '''
269  return self._container_container
270 
271  def __str__(self):
272  return "PSy"
273 
274  @property
275  def invokes(self):
276  ''':returns: the list of invokes.
277  :rtype: :py:class:`psyclone.psyGen.Invokes` or derived class
278  '''
279  return self._invokes_invokes
280 
281  @property
282  def name(self):
283  ''':returns: the name of the PSy object.
284  :rtype: str
285  '''
286  return "psy_"+self._name_name
287 
288  @property
289  @abc.abstractmethod
290  def gen(self):
291  '''Abstract base class for code generation function.
292 
293  :returns: root node of generated Fortran AST.
294  :rtype: :py:class:`psyclone.psyir.nodes.Node`
295  '''
296 
297 
298 class Invokes():
299  '''Manage the invoke calls.
300 
301  :param alg_calls: a list of invoke metadata extracted by the \
302  parser.
303  :type alg_calls: list of \
304  :py:class:`psyclone.parse.algorithm.InvokeCall`
305  :param invoke_cls: an api-specific Invoke class.
306  :type invoke_cls: subclass of :py:class:`psyclone.psyGen.Invoke`
307  :param psy: the PSy instance containing this Invokes instance.
308  :type psy: subclass of :py:class`psyclone.psyGen.PSy`
309 
310  '''
311  def __init__(self, alg_calls, invoke_cls, psy):
312  self._psy_psy = psy
313  self.invoke_mapinvoke_map = {}
314  self.invoke_listinvoke_list = []
315  for idx, alg_invocation in enumerate(alg_calls):
316  my_invoke = invoke_cls(alg_invocation, idx, self)
317  self.invoke_mapinvoke_map[my_invoke.name] = my_invoke
318  self.invoke_listinvoke_list.append(my_invoke)
319 
320  def __str__(self):
321  return "Invokes object containing "+str(self.namesnames)
322 
323  @property
324  def psy(self):
325  '''
326  :returns: the PSy instance that contains this instance.
327  :rtype: subclass of :py:class:`psyclone.psyGen.PSy`
328 
329  '''
330  return self._psy_psy
331 
332  @property
333  def names(self):
334  return self.invoke_mapinvoke_map.keys()
335 
336  def get(self, invoke_name):
337  '''
338  Gets the Invoke with the supplied name. If the name does not already
339  begin with ``invoke_`` then a new name with this prepended is included
340  in the search if no exact match is found initially.
341 
342  :param str invoke_name: the name of the Invoke to get (not case-
343  sensitive).
344 
345  :returns: the invoke with the specified name.
346  :rtype: :py:class:`psyclone.psyGen.Invoke`
347 
348  :raises RuntimeError: if no Invoke with the supplied name (with or
349  without ``invoke_`` prepended) exists.
350  '''
351  search_names = [invoke_name.lower()]
352  if not search_names[0].startswith("invoke_"):
353  search_names.append("invoke_"+search_names[0])
354  for name in search_names:
355  try:
356  return self.invoke_mapinvoke_map[name]
357  except KeyError:
358  pass
359 
360  search_list = " or ".join(f"'{name}'" for name in search_names)
361  raise RuntimeError(f"Cannot find an invoke named {search_list} "
362  f"in {list(self.names)}")
363 
364  def gen_code(self, parent):
365  '''
366  Create the f2pygen AST for each Invoke in the PSy layer.
367 
368  :param parent: the parent node in the AST to which to add content.
369  :type parent: `psyclone.f2pygen.ModuleGen`
370 
371  :raises GenerationError: if an invoke_list schedule is not an \
372  InvokeSchedule.
373  '''
374  for invoke in self.invoke_listinvoke_list:
375  if not isinstance(invoke.schedule, InvokeSchedule):
376  raise GenerationError(
377  f"An invoke.schedule element of the invoke_list is a "
378  f"'{type(invoke.schedule).__name__}', but it should be an "
379  f"'InvokeSchedule'.")
380  invoke.gen_code(parent)
381 
382 
383 class Invoke():
384  r'''Manage an individual invoke call.
385 
386  :param alg_invocation: metadata from the parsed code capturing \
387  information for this Invoke instance.
388  :type alg_invocation: :py:class:`psyclone.parse.algorithm.InvokeCall`
389  :param int idx: position/index of this invoke call in the subroutine. \
390  If not None, this number is added to the name ("invoke\_").
391  :param schedule_class: the schedule class to create for this invoke.
392  :type schedule_class: :py:class:`psyclone.psyGen.InvokeSchedule`
393  :param invokes: the Invokes instance that contains this Invoke \
394  instance.
395  :type invokes: :py:class:`psyclone.psyGen.Invokes`
396  :param reserved_names: optional list of reserved names, i.e. names that \
397  should not be used e.g. as a PSyclone-created \
398  variable name.
399  :type reserved_names: list of str
400 
401  '''
402  def __init__(self, alg_invocation, idx, schedule_class, invokes,
403  reserved_names=None):
404  '''Construct an invoke object.'''
405 
406  self._invokes_invokes = invokes
407  self._name_name = "invoke"
408  self._alg_unique_args_alg_unique_args = []
409 
410  if alg_invocation is None and idx is None:
411  return
412 
413  # create a name for the call if one does not already exist
414  if alg_invocation.name is not None:
415  # In Python2 unicode strings must be converted to str()
416  self._name_name = str(alg_invocation.name)
417  elif len(alg_invocation.kcalls) == 1 and \
418  alg_invocation.kcalls[0].type == "kernelCall":
419  # use the name of the kernel call with the position appended.
420  # Appended position is needed in case we have two separate invokes
421  # in the same algorithm code containing the same (single) kernel
422  self._name_name = "invoke_" + str(idx) + "_" + \
423  alg_invocation.kcalls[0].ktype.name
424  else:
425  # use the position of the invoke
426  self._name_name = "invoke_" + str(idx)
427 
428  if not reserved_names:
429  reserved_names = []
430 
431  # Get a reference to the parent container, if any
432  container = None
433  if self.invokesinvokes:
434  container = self.invokesinvokes.psy.container
435 
436  # create the schedule
437  self._schedule_schedule = schedule_class(self._name_name, alg_invocation.kcalls,
438  reserved_names, parent=container)
439 
440  # Add the new Schedule to the top-level PSy Container
441  if container:
442  container.addchild(self._schedule_schedule)
443 
444  # let the schedule have access to me
445  self._schedule_schedule.invoke = self
446 
447  # extract the argument list for the algorithm call and psy
448  # layer subroutine.
449  self._alg_unique_args_alg_unique_args = []
450  self._psy_unique_vars_psy_unique_vars = []
451  tmp_arg_names = []
452  for call in self.schedulescheduleschedule.kernels():
453  for arg in call.arguments.args:
454  if arg.text is not None:
455  if arg.text not in self._alg_unique_args_alg_unique_args:
456  self._alg_unique_args_alg_unique_args.append(arg.text)
457  if arg.name not in tmp_arg_names:
458  tmp_arg_names.append(arg.name)
459  self._psy_unique_vars_psy_unique_vars.append(arg)
460  else:
461  # literals have no name
462  pass
463 
464  # work out the unique dofs required in this subroutine
465  self._dofs_dofs = {}
466  for kern_call in self._schedule_schedule.coded_kernels():
467  dofs = kern_call.arguments.dofs
468  for dof in dofs:
469  if dof not in self._dofs_dofs:
470  # Only keep the first occurrence for the moment. We will
471  # need to change this logic at some point as we need to
472  # cope with writes determining the dofs that are used.
473  self._dofs_dofs[dof] = [kern_call, dofs[dof][0]]
474 
475  def __str__(self):
476  return self._name_name+"("+", ".join([str(arg) for arg in
477  self._alg_unique_args_alg_unique_args])+")"
478 
479  @property
480  def invokes(self):
481  '''
482  :returns: the Invokes instance that contains this instance.
483  :rtype: :py:class`psyclone.psyGen.Invokes`
484 
485  '''
486  return self._invokes_invokes
487 
488  @property
489  def name(self):
490  return self._name_name
491 
492  @property
493  def alg_unique_args(self):
494  return self._alg_unique_args_alg_unique_args
495 
496  @property
497  def psy_unique_vars(self):
498  return self._psy_unique_vars_psy_unique_vars
499 
500  @property
501  def psy_unique_var_names(self):
502  names = []
503  for var in self._psy_unique_vars_psy_unique_vars:
504  names.append(var.name)
505  return names
506 
507  @property
508  def schedule(self):
509  return self._schedule_schedule
510 
511  @schedule.setter
512  def schedule(self, obj):
513  self._schedule_schedule = obj
514 
515  def unique_declarations(self, argument_types, access=None,
516  intrinsic_type=None):
517  '''
518  Returns a list of all required declarations for the specified
519  API argument types. If access is supplied (e.g. "write") then
520  only declarations with that access are returned. If an intrinsic
521  type is supplied then only declarations with that intrinsic type
522  are returned.
523 
524  :param argument_types: the types of the kernel argument for the \
525  particular API.
526  :type argument_types: list of str
527  :param access: optional AccessType that the declaration should have.
528  :type access: :py:class:`psyclone.core.access_type.AccessType`
529  :param intrinsic_type: optional intrinsic type of argument data.
530  :type intrinsic_type: str
531 
532  :returns: a list of all declared kernel arguments.
533  :rtype: list of :py:class:`psyclone.psyGen.KernelArgument`
534 
535  :raises InternalError: if at least one kernel argument type is \
536  not valid for the particular API.
537  :raises InternalError: if an invalid access is specified.
538  :raises InternalError: if an invalid intrinsic type is specified.
539 
540  '''
541  # First check for invalid argument types, access and intrinsic type
542  const = Config.get().api_conf().get_constants()
543  if any(argtype not in const.VALID_ARG_TYPE_NAMES for
544  argtype in argument_types):
545  raise InternalError(
546  f"Invoke.unique_declarations() called with at least one "
547  f"invalid argument type. Expected one of "
548  f"{const.VALID_ARG_TYPE_NAMES} but found {argument_types}.")
549 
550  if access and not isinstance(access, AccessType):
551  raise InternalError(
552  f"Invoke.unique_declarations() called with an invalid "
553  f"access type. Type is '{access}' instead of AccessType.")
554 
555  if (intrinsic_type and intrinsic_type not in
556  const.VALID_INTRINSIC_TYPES):
557  raise InternalError(
558  f"Invoke.unique_declarations() called with an invalid "
559  f"intrinsic argument data type. Expected one of "
560  f"{const.VALID_INTRINSIC_TYPES} but found '{intrinsic_type}'.")
561 
562  # Initialise dictionary of kernel arguments to get the
563  # argument list from
564  declarations = OrderedDict()
565  # Find unique kernel arguments using their declaration names
566  for call in self.schedulescheduleschedule.kernels():
567  for arg in call.arguments.args:
568  if not intrinsic_type or arg.intrinsic_type == intrinsic_type:
569  if not access or arg.access == access:
570  if arg.text is not None:
571  if arg.argument_type in argument_types:
572  test_name = arg.declaration_name
573  if test_name not in declarations:
574  declarations[test_name] = arg
575  return list(declarations.values())
576 
577  def first_access(self, arg_name):
578  ''' Returns the first argument with the specified name passed to
579  a kernel in our schedule '''
580  for call in self.schedulescheduleschedule.kernels():
581  for arg in call.arguments.args:
582  if arg.text is not None:
583  if arg.declaration_name == arg_name:
584  return arg
585  raise GenerationError(f"Failed to find any kernel argument with name "
586  f"'{arg_name}'")
587 
588  def unique_declns_by_intent(self, argument_types, intrinsic_type=None):
589  '''
590  Returns a dictionary listing all required declarations for each
591  type of intent ('inout', 'out' and 'in').
592 
593  :param argument_types: the types of the kernel argument for the \
594  particular API for which the intent is required.
595  :type argument_types: list of str
596  :param intrinsic_type: optional intrinsic type of argument data.
597  :type intrinsic_type: str
598 
599  :returns: dictionary containing 'intent' keys holding the kernel \
600  arguments as values for each type of intent.
601  :rtype: dict of :py:class:`psyclone.psyGen.KernelArgument`
602 
603  :raises InternalError: if at least one kernel argument type is \
604  not valid for the particular API.
605  :raises InternalError: if an invalid intrinsic type is specified.
606 
607  '''
608  # First check for invalid argument types and intrinsic type
609  const = Config.get().api_conf().get_constants()
610  if any(argtype not in const.VALID_ARG_TYPE_NAMES for
611  argtype in argument_types):
612  raise InternalError(
613  f"Invoke.unique_declns_by_intent() called with at least one "
614  f"invalid argument type. Expected one of "
615  f"{const.VALID_ARG_TYPE_NAMES} but found {argument_types}.")
616 
617  if (intrinsic_type and intrinsic_type not in
618  const.VALID_INTRINSIC_TYPES):
619  raise InternalError(
620  f"Invoke.unique_declns_by_intent() called with an invalid "
621  f"intrinsic argument data type. Expected one of "
622  f"{const.VALID_INTRINSIC_TYPES} but found '{intrinsic_type}'.")
623 
624  # We will return a dictionary containing as many lists
625  # as there are types of intent
626  declns = {}
627  for intent in FORTRAN_INTENT_NAMES:
628  declns[intent] = []
629 
630  for arg in self.unique_declarationsunique_declarations(argument_types,
631  intrinsic_type=intrinsic_type):
632  first_arg = self.first_accessfirst_access(arg.declaration_name)
633  if first_arg.access in [AccessType.WRITE, AccessType.SUM]:
634  # If the first access is a write then the intent is
635  # out irrespective of any other accesses. Note,
636  # sum_args behave as if they are write_args from the
637  # PSy-layer's perspective.
638  declns["out"].append(arg)
639  continue
640  # if all accesses are read, then the intent is in,
641  # otherwise the intent is inout (as we have already
642  # dealt with intent out).
643  read_only = True
644  for call in self.schedulescheduleschedule.kernels():
645  for tmp_arg in call.arguments.args:
646  if tmp_arg.text is not None and \
647  tmp_arg.declaration_name == arg.declaration_name:
648  if tmp_arg.access != AccessType.READ:
649  # readwrite_args behave in the
650  # same way as inc_args from the
651  # perspective of intents
652  read_only = False
653  break
654  if not read_only:
655  break
656  if read_only:
657  declns["in"].append(arg)
658  else:
659  declns["inout"].append(arg)
660  return declns
661 
662  def gen(self):
663  from psyclone.f2pygen import ModuleGen
664  module = ModuleGen("container")
665  self.gen_codegen_code(module)
666  return module.root
667 
668  @abc.abstractmethod
669  def gen_code(self, parent):
670  '''
671  Generates invocation code (the subroutine called by the associated
672  invoke call in the algorithm layer). This consists of the PSy
673  invocation subroutine and the declaration of its arguments.
674 
675  :param parent: the node in the generated AST to which to add content.
676  :type parent: :py:class:`psyclone.f2pygen.ModuleGen`
677 
678  '''
679 
680 
681 class InvokeSchedule(Routine):
682  '''
683  Stores schedule information for an invocation call. Schedules can be
684  optimised using transformations.
685 
686  >>> from psyclone.parse.algorithm import parse
687  >>> ast, info = parse("algorithm.f90")
688  >>> from psyclone.psyGen import PSyFactory
689  >>> api = "..."
690  >>> psy = PSyFactory(api).create(info)
691  >>> invokes = psy.invokes
692  >>> invokes.names
693  >>> invoke = invokes.get("name")
694  >>> schedule = invoke.schedule
695  >>> print(schedule.view())
696 
697  :param str name: name of the Invoke.
698  :param type KernFactory: class instance of the factory to use when \
699  creating Kernels. e.g. \
700  :py:class:`psyclone.domain.lfric.LFRicKernCallFactory`.
701  :param type BuiltInFactory: class instance of the factory to use when \
702  creating built-ins. e.g. \
703  :py:class:`psyclone.domain.lfric.lfric_builtins.LFRicBuiltInCallFactory`.
704  :param alg_calls: list of Kernel calls in the schedule.
705  :type alg_calls: list of :py:class:`psyclone.parse.algorithm.KernelCall`
706  :param kwargs: additional keyword arguments provided to the super class.
707  :type kwargs: unwrapped dict.
708 
709  '''
710  # Textual description of the node.
711  _text_name = "InvokeSchedule"
712 
713  def __init__(self, name, KernFactory, BuiltInFactory, alg_calls=None,
714  reserved_names=None, **kwargs):
715  super().__init__(name, **kwargs)
716 
717  self._invoke = None
718 
719  # Populate the Schedule Symbol Table with the reserved names.
720  if reserved_names:
721  for reserved in reserved_names:
722  self.symbol_table.add(Symbol(reserved))
723 
724  # We need to separate calls into loops (an iteration space really)
725  # and calls so that we can perform optimisations separately on the
726  # two entities.
727  if alg_calls is None:
728  alg_calls = []
729  for call in alg_calls:
730  if isinstance(call, BuiltInCall):
731  self.addchild(BuiltInFactory.create(call, parent=self))
732  else:
733  self.addchild(KernFactory.create(call, parent=self))
734 
735  @property
736  def symbol_table(self):
737  '''
738  :returns: Table containing symbol information for the schedule.
739  :rtype: :py:class:`psyclone.psyir.symbols.SymbolTable`
740  '''
741  return self._symbol_table
742 
743  @property
744  def invoke(self):
745  return self._invoke_invoke
746 
747  @invoke.setter
748  def invoke(self, my_invoke):
749  self._invoke_invoke = my_invoke
750 
751  def node_str(self, colour=True):
752  '''
753  Returns the name of this node with appropriate control codes
754  to generate coloured output in a terminal that supports it.
755 
756  :param bool colour: whether or not to include colour control codes.
757 
758  :returns: description of this node, possibly coloured.
759  :rtype: str
760  '''
761  return f"{self.coloured_name(colour)}[invoke='{self.name}']"
762 
763  def __str__(self):
764  result = self.coloured_name(False) + ":\n"
765  for entity in self._children:
766  result += str(entity) + "\n"
767  result += "End " + self.coloured_name(False) + "\n"
768  return result
769 
770  def gen_code(self, parent):
771  '''
772  Generate the Nodes in the f2pygen AST for this schedule.
773 
774  :param parent: the parent Node (i.e. the enclosing subroutine) to \
775  which to add content.
776  :type parent: :py:class:`psyclone.f2pygen.SubroutineGen`
777 
778  '''
779  # Imported symbols promoted from Kernel imports are in the SymbolTable.
780  # First aggregate all variables imported from the same module in a map.
781  module_map = {}
782  for imported_var in self.symbol_tablesymbol_table.imported_symbols:
783  module_name = imported_var.interface.container_symbol.name
784  if module_name in module_map:
785  module_map[module_name].append(imported_var.name)
786  else:
787  module_map[module_name] = [imported_var.name]
788 
789  # Then we can produce the UseGen statements without repeating modules
790  for module_name, var_list in module_map.items():
791  parent.add(UseGen(parent, name=module_name, only=True,
792  funcnames=var_list))
793 
794  for entity in self.children:
795  entity.gen_code(parent)
796 
797 
798 class GlobalSum(Statement):
799  '''
800  Generic Global Sum class which can be added to and manipulated
801  in, a schedule.
802 
803  :param scalar: the scalar that the global sum is stored into
804  :type scalar: :py:class:`psyclone.dynamo0p3.DynKernelArgument`
805  :param parent: optional parent (default None) of this object
806  :type parent: :py:class:`psyclone.psyir.nodes.Node`
807 
808  '''
809  # Textual description of the node.
810  _children_valid_format = "<LeafNode>"
811  _text_name = "GlobalSum"
812  _colour = "cyan"
813 
814  def __init__(self, scalar, parent=None):
815  Node.__init__(self, children=[], parent=parent)
816  import copy
817  self._scalar_scalar = copy.copy(scalar)
818  if scalar:
819  # Update scalar values appropriately
820  # Here "readwrite" denotes how the class GlobalSum
821  # accesses/updates a scalar
822  self._scalar_scalar.access = AccessType.READWRITE
823  self._scalar_scalar.call = self
824 
825  @property
826  def scalar(self):
827  ''' Return the scalar field that this global sum acts on '''
828  return self._scalar_scalar
829 
830  @property
831  def dag_name(self):
832  '''
833  :returns: the name to use in the DAG for this node.
834  :rtype: str
835  '''
836  return f"globalsum({self._scalar.name})_{self.position}"
837 
838  @property
839  def args(self):
840  ''' Return the list of arguments associated with this node. Override
841  the base method and simply return our argument.'''
842  return [self._scalar_scalar]
843 
844  def node_str(self, colour=True):
845  '''
846  Returns a text description of this node with (optional) control codes
847  to generate coloured output in a terminal that supports it.
848 
849  :param bool colour: whether or not to include colour control codes.
850 
851  :returns: description of this node, possibly coloured.
852  :rtype: str
853  '''
854  return f"{self.coloured_name(colour)}[scalar='{self._scalar.name}']"
855 
856 
857 class HaloExchange(Statement):
858  '''
859  Generic Halo Exchange class which can be added to and
860  manipulated in, a schedule.
861 
862  :param field: the field that this halo exchange will act on
863  :type field: :py:class:`psyclone.dynamo0p3.DynKernelArgument`
864  :param check_dirty: optional argument default True indicating whether \
865  this halo exchange should be subject to a run-time \
866  check for clean/dirty halos.
867  :type check_dirty: bool
868  :param vector_index: optional vector index (default None) to identify \
869  which index of a vector field this halo exchange is \
870  responsible for.
871  :type vector_index: int
872  :param parent: optional parent (default None) of this object
873  :type parent: :py:class:`psyclone.psyir.nodes.Node`
874 
875  '''
876  # Textual description of the node.
877  _children_valid_format = "<LeafNode>"
878  _text_name = "HaloExchange"
879  _colour = "blue"
880 
881  def __init__(self, field, check_dirty=True,
882  vector_index=None, parent=None):
883  Node.__init__(self, children=[], parent=parent)
884  import copy
885  self._field_field = copy.copy(field)
886  if field:
887  # Update fields values appropriately
888  # Here "readwrite" denotes how the class HaloExchange
889  # accesses a field rather than the field's continuity
890  self._field_field.access = AccessType.READWRITE
891  self._field_field.call = self
892  self._halo_type_halo_type = None
893  self._halo_depth_halo_depth = None
894  self._check_dirty_check_dirty = check_dirty
895  self._vector_index_vector_index = vector_index
896  # Keep a reference to the SymbolTable associated with the
897  # InvokeSchedule.
898  self._symbol_table_symbol_table = None
899  isched = self.ancestor(InvokeSchedule)
900  if isched:
901  self._symbol_table_symbol_table = isched.symbol_table
902 
903  @property
904  def vector_index(self):
905  '''If the field is a vector then return the vector index associated
906  with this halo exchange. Otherwise return None'''
907  return self._vector_index_vector_index
908 
909  @property
910  def halo_depth(self):
911  ''' Return the depth of the halo exchange '''
912  return self._halo_depth_halo_depth
913 
914  @halo_depth.setter
915  def halo_depth(self, value):
916  ''' Set the depth of the halo exchange '''
917  self._halo_depth_halo_depth = value
918 
919  @property
920  def field(self):
921  ''' Return the field that the halo exchange acts on '''
922  return self._field_field
923 
924  @property
925  def dag_name(self):
926  '''
927  :returns: the name to use in a dag for this node.
928  :rtype: str
929  '''
930  name = f"{self._text_name}({self._field.name})_{self.position}"
931  if self._check_dirty_check_dirty:
932  name = "check" + name
933  return name
934 
935  @property
936  def args(self):
937  '''Return the list of arguments associated with this node. Overide the
938  base method and simply return our argument. '''
939  return [self._field_field]
940 
941  def check_vector_halos_differ(self, node):
942  '''Helper method which checks that two halo exchange nodes (one being
943  self and the other being passed by argument) operating on the
944  same field, both have vector fields of the same size and use
945  different vector indices. If this is the case then the halo
946  exchange nodes do not depend on each other. If this is not the
947  case then an internal error will have occured and we raise an
948  appropriate exception.
949 
950  :param node: a halo exchange which should exchange the same field as \
951  self.
952  :type node: :py:class:`psyclone.psyGen.HaloExchange`
953  :raises GenerationError: if the argument passed is not a halo exchange.
954  :raises GenerationError: if the field name in the halo exchange \
955  passed in has a different name to the field \
956  in this halo exchange.
957  :raises GenerationError: if the field in this halo exchange is not a \
958  vector field
959  :raises GenerationError: if the vector size of the field in this halo \
960  exchange is different to vector size of the \
961  field in the halo exchange passed by argument.
962  :raises GenerationError: if the vector index of the field in this \
963  halo exchange is the same as the vector \
964  index of the field in the halo exchange \
965  passed by argument.
966 
967  '''
968 
969  if not isinstance(node, HaloExchange):
970  raise GenerationError(
971  "Internal error, the argument passed to "
972  "HaloExchange.check_vector_halos_differ() is not "
973  "a halo exchange object")
974 
975  if self.fieldfield.name != node.field.name:
976  raise GenerationError(
977  f"Internal error, the halo exchange object passed to "
978  f"HaloExchange.check_vector_halos_differ() has a different "
979  f"field name '{node.field.name}' to self '{self.field.name}'")
980 
981  if self.fieldfield.vector_size <= 1:
982  raise GenerationError(
983  "Internal error, HaloExchange.check_vector_halos_differ() "
984  "a halo exchange depends on another halo exchange but the "
985  f"vector size of field '{self.field.name}' is 1")
986 
987  if self.fieldfield.vector_size != node.field.vector_size:
988  raise GenerationError(
989  f"Internal error, HaloExchange.check_vector_halos_differ() "
990  f"a halo exchange depends on another halo exchange but the "
991  f"vector sizes for field '{self.field.name}' differ")
992 
993  if self.vector_indexvector_indexvector_index == node.vector_index:
994  raise GenerationError(
995  f"Internal error, HaloExchange.check_vector_halos_differ() "
996  f"a halo exchange depends on another halo exchange but both "
997  f"vector id's ('{self.vector_index}') of field "
998  f"'{self.field.name}' are the same")
999 
1000  def node_str(self, colour=True):
1001  '''
1002  Returns the name of this node with (optional) control codes
1003  to generate coloured output in a terminal that supports it.
1004 
1005  :param bool colour: whether or not to include colour control codes.
1006 
1007  :returns: description of this node, possibly coloured.
1008  :rtype: str
1009  '''
1010  return (f"{self.coloured_name(colour)}[field='{self._field.name}', "
1011  f"type='{self._halo_type}', depth={self._halo_depth}, "
1012  f"check_dirty={self._check_dirty}]")
1013 
1014 
1015 class Kern(Statement):
1016  '''Base class representing a call to a sub-program unit from within the
1017  PSy layer. It is possible for this unit to be in-lined within the
1018  PSy layer.
1019 
1020  :param parent: parent of this node in the PSyIR.
1021  :type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`
1022  :param call: information on the call itself, as obtained by parsing \
1023  the Algorithm layer code.
1024  :type call: :py:class:`psyclone.parse.algorithm.KernelCall`
1025  :param str name: the name of the routine being called.
1026  :param ArgumentsClass: class to create the object that holds all \
1027  information on the kernel arguments, as extracted from kernel \
1028  meta-data (and accessible here via call.ktype).
1029  :type ArgumentsClass: type of :py:class:`psyclone.psyGen.Arguments`
1030  :param bool check: whether to check for consistency between the \
1031  kernel metadata and the algorithm layer. Defaults to True.
1032 
1033  :raises GenerationError: if any of the arguments to the call are \
1034  duplicated.
1035 
1036  '''
1037  # Textual representation of the valid children for this node.
1038  _children_valid_format = "<LeafNode>"
1039 
1040  def __init__(self, parent, call, name, ArgumentsClass, check=True):
1041  # pylint: disable=too-many-arguments
1042  super().__init__(parent=parent)
1043  self._name_name = name
1044  self._iterates_over_iterates_over = call.ktype.iterates_over
1045  self._arguments_arguments = ArgumentsClass(call, self, check=check)
1046 
1047  # check algorithm arguments are unique for a kernel or
1048  # built-in call
1049  arg_names = []
1050  for arg in self._arguments_arguments.args:
1051  if arg.text:
1052  text = arg.text.lower().replace(" ", "")
1053  if text in arg_names:
1054  raise GenerationError(
1055  f"Argument '{arg.text}' is passed into kernel "
1056  f"'{self._name}' code more than once from the "
1057  f"algorithm layer. This is not allowed.")
1058  arg_names.append(text)
1059 
1060  self._arg_descriptors_arg_descriptors = None
1061 
1062  # Initialise any reduction information
1063  reduction_modes = AccessType.get_valid_reduction_modes()
1064  const = Config.get().api_conf().get_constants()
1065  args = args_filter(self._arguments_arguments.args,
1066  arg_types=const.VALID_SCALAR_NAMES,
1067  arg_accesses=reduction_modes)
1068  if args:
1069  self._reduction_reduction = True
1070  if len(args) != 1:
1071  raise GenerationError(
1072  "PSyclone currently only supports a single reduction "
1073  "in a kernel or builtin")
1074  self._reduction_arg_reduction_arg = args[0]
1075  else:
1076  self._reduction_reduction = False
1077  self._reduction_arg_reduction_arg = None
1078 
1079  @property
1080  def args(self):
1081  '''Return the list of arguments associated with this node. Overide the
1082  base method and simply return our arguments. '''
1083  return self.argumentsarguments.args
1084 
1085  def node_str(self, colour=True):
1086  ''' Returns the name of this node with (optional) control codes
1087  to generate coloured output in a terminal that supports it.
1088 
1089  :param bool colour: whether or not to include colour control codes.
1090 
1091  :returns: description of this node, possibly coloured.
1092  :rtype: str
1093  '''
1094  return (self.coloured_name(colour) + " " + self.namenamename +
1095  "(" + self.argumentsarguments.names + ")")
1096 
1097  def reference_accesses(self, var_accesses):
1098  '''Get all variable access information. The API specific classes
1099  add the accesses to the arguments. So the code here only calls
1100  the baseclass, and increases the location.
1101 
1102  :param var_accesses: VariablesAccessInfo instance that stores the \
1103  information about variable accesses.
1104  :type var_accesses: \
1105  :py:class:`psyclone.core.VariablesAccessInfo`
1106  '''
1107  super().reference_accesses(var_accesses)
1108  var_accesses.next_location()
1109 
1110  @property
1111  def is_reduction(self):
1112  '''
1113  :returns: whether this kernel/built-in contains a reduction variable.
1114  :rtype: bool
1115 
1116  '''
1117  return self._reduction_reduction
1118 
1119  @property
1120  def reduction_arg(self):
1121  '''
1122  :returns: the reduction variable if this kernel/built-in
1123  contains one and `None` otherwise.
1124  :rtype: :py:class:`psyclone.psyGen.KernelArgument` or `NoneType`
1125 
1126  '''
1127  return self._reduction_arg_reduction_arg
1128 
1129  @property
1130  def reprod_reduction(self):
1131  '''
1132  :returns: whether this kernel/built-in is enclosed within an OpenMP
1133  do loop. If so report whether it has the reproducible flag
1134  set. Note, this also catches OMPParallelDo Directives but
1135  they have reprod set to False so it is OK.
1136  :rtype: bool
1137 
1138  '''
1139  ancestor = self.ancestor(OMPDoDirective)
1140  if ancestor:
1141  return ancestor.reprod
1142  return False
1143 
1144  @property
1146  '''
1147  :returns: a local reduction variable name that is unique for the
1148  current reduction argument name. This is used for
1149  thread-local reductions with reproducible reductions.
1150  :rtype: str
1151 
1152  '''
1153  # TODO #2381: Revisit symbol creation, now moved to the
1154  # Kern._reduction_reference() method, and try to associate it
1155  # with the PSy-layer generation or relevant transformation.
1156  return "l_" + self.reduction_argreduction_arg.name
1157 
1158  def zero_reduction_variable(self, parent, position=None):
1159  '''
1160  Generate code to zero the reduction variable and to zero the local
1161  reduction variable if one exists. The latter is used for reproducible
1162  reductions, if specified.
1163 
1164  :param parent: the Node in the AST to which to add new code.
1165  :type parent: :py:class:`psyclone.psyir.nodes.Node`
1166  :param str position: where to position the new code in the AST.
1167 
1168  :raises GenerationError: if the variable to zero is not a scalar.
1169  :raises GenerationError: if the reprod_pad_size (read from the \
1170  configuration file) is less than 1.
1171  :raises GenerationError: for a reduction into a scalar that is \
1172  neither 'real' nor 'integer'.
1173 
1174  '''
1175  if not position:
1176  position = ["auto"]
1177  var_name = self._reduction_arg_reduction_arg.name
1178  local_var_name = self.local_reduction_namelocal_reduction_name
1179  var_arg = self._reduction_arg_reduction_arg
1180  # Check for a non-scalar argument
1181  if not var_arg.is_scalar:
1182  raise GenerationError(
1183  f"Kern.zero_reduction_variable() should be a scalar but "
1184  f"found '{var_arg.argument_type}'.")
1185  # Generate the reduction variable
1186  var_data_type = var_arg.intrinsic_type
1187  if var_data_type == "real":
1188  data_value = "0.0"
1189  elif var_data_type == "integer":
1190  data_value = "0"
1191  else:
1192  raise GenerationError(
1193  f"Kern.zero_reduction_variable() should be either a 'real' or "
1194  f"an 'integer' scalar but found scalar of type "
1195  f"'{var_arg.intrinsic_type}'.")
1196  # Retrieve the precision information (if set) and append it
1197  # to the initial reduction value
1198  if var_arg.precision:
1199  kind_type = var_arg.precision
1200  zero_sum_variable = "_".join([data_value, kind_type])
1201  else:
1202  kind_type = ""
1203  zero_sum_variable = data_value
1204  parent.add(AssignGen(parent, lhs=var_name, rhs=zero_sum_variable),
1205  position=position)
1206  if self.reprod_reductionreprod_reduction:
1207  parent.add(DeclGen(parent, datatype=var_data_type,
1208  entity_decls=[local_var_name],
1209  allocatable=True, kind=kind_type,
1210  dimension=":,:"))
1211  nthreads = \
1212  self.scope.symbol_table.lookup_with_tag("omp_num_threads").name
1213  if Config.get().reprod_pad_size < 1:
1214  raise GenerationError(
1215  f"REPROD_PAD_SIZE in {Config.get().filename} should be a "
1216  f"positive integer, but it is set to "
1217  f"'{Config.get().reprod_pad_size}'.")
1218  pad_size = str(Config.get().reprod_pad_size)
1219  parent.add(AllocateGen(parent, local_var_name + "(" + pad_size +
1220  "," + nthreads + ")"), position=position)
1221  parent.add(AssignGen(parent, lhs=local_var_name,
1222  rhs=zero_sum_variable), position=position)
1223 
1224  def reduction_sum_loop(self, parent):
1225  '''
1226  Generate the appropriate code to place after the end parallel
1227  region.
1228 
1229  :param parent: the Node in the f2pygen AST to which to add new code.
1230  :type parent: :py:class:`psyclone.f2pygen.SubroutineGen`
1231 
1232  :raises GenerationError: for an unsupported reduction access in \
1233  LFRicBuiltIn.
1234 
1235  '''
1236  var_name = self._reduction_arg_reduction_arg.name
1237  local_var_name = self.local_reduction_namelocal_reduction_name
1238  # A non-reproducible reduction requires a single-valued argument
1239  local_var_ref = self._reduction_reference_reduction_reference().name
1240  # A reproducible reduction requires multi-valued argument stored
1241  # as a padded array separately for each thread
1242  if self.reprod_reductionreprod_reduction:
1243  local_var_ref = FortranWriter().arrayreference_node(
1244  self._reduction_reference_reduction_reference())
1245  reduction_access = self._reduction_arg_reduction_arg.access
1246  try:
1247  reduction_operator = REDUCTION_OPERATOR_MAPPING[reduction_access]
1248  except KeyError as err:
1249  api_strings = [access.api_specific_name()
1250  for access in REDUCTION_OPERATOR_MAPPING]
1251  raise GenerationError(
1252  f"Unsupported reduction access "
1253  f"'{reduction_access.api_specific_name()}' found in "
1254  f"LFRicBuiltIn:reduction_sum_loop(). Expected one of "
1255  f"{api_strings}.") from err
1256  symtab = self.scope.symbol_table
1257  thread_idx = symtab.lookup_with_tag("omp_thread_index").name
1258  nthreads = symtab.lookup_with_tag("omp_num_threads").name
1259  do_loop = DoGen(parent, thread_idx, "1", nthreads)
1260  do_loop.add(AssignGen(do_loop, lhs=var_name, rhs=var_name +
1261  reduction_operator + local_var_ref))
1262  parent.add(do_loop)
1263  parent.add(DeallocateGen(parent, local_var_name))
1264 
1265  def _reduction_reference(self):
1266  '''
1267  Return the reference to the reduction variable if OpenMP is set to
1268  be unreproducible, as we will be using the OpenMP reduction clause.
1269  Otherwise we will be computing the reduction ourselves and therefore
1270  need to store values into a (padded) array separately for each
1271  thread.
1272 
1273  :returns: reference to the variable to be reduced.
1274  :rtype: :py:class:`psyclone.psyir.nodes.Reference` or
1275  :py:class:`psyclone.psyir.nodes.ArrayReference`
1276 
1277  '''
1278  # TODO #2381: Revisit symbol creation, moved from the
1279  # Kern.local_reduction_name property, and try to associate it
1280  # with the PSy-layer generation or relevant transformation.
1281  symtab = self.scope.symbol_table
1282  reduction_name = self.reduction_argreduction_arg.name
1283  # Return a multi-valued ArrayReference for a reproducible reduction
1284  if self.reprod_reductionreprod_reduction:
1285  array_dim = [
1286  Literal("1", INTEGER_TYPE),
1287  Reference(symtab.lookup_with_tag("omp_thread_index"))]
1288  reduction_array = ArrayType(
1289  symtab.lookup(reduction_name).datatype, array_dim)
1290  local_reduction = DataSymbol(
1291  self.local_reduction_namelocal_reduction_name, datatype=reduction_array)
1292  symtab.find_or_create_tag(
1293  tag=self.local_reduction_namelocal_reduction_name,
1294  symbol_type=DataSymbol, datatype=reduction_array)
1295  return ArrayReference.create(
1296  local_reduction, array_dim)
1297  # Return a single-valued Reference for a non-reproducible reduction
1298  return Reference(symtab.lookup(reduction_name))
1299 
1300  @property
1301  def arg_descriptors(self):
1302  return self._arg_descriptors_arg_descriptors
1303 
1304  @arg_descriptors.setter
1305  def arg_descriptors(self, obj):
1306  self._arg_descriptors_arg_descriptors = obj
1307 
1308  @property
1309  def arguments(self):
1310  return self._arguments_arguments
1311 
1312  @property
1313  def name(self):
1314  '''
1315  :returns: the name of the kernel.
1316  :rtype: str
1317  '''
1318  return self._name_name
1319 
1320  @name.setter
1321  def name(self, value):
1322  '''
1323  Set the name of the kernel.
1324 
1325  :param str value: The name of the kernel.
1326  '''
1327  self._name_name = value
1328 
1329  def is_coloured(self):
1330  '''
1331  :returns: True if this kernel is being called from within a \
1332  coloured loop.
1333  :rtype: bool
1334  '''
1335  parent_loop = self.ancestor(Loop)
1336  while parent_loop:
1337  if parent_loop.loop_type == "colour":
1338  return True
1339  parent_loop = parent_loop.ancestor(Loop)
1340  return False
1341 
1342  @property
1343  def iterates_over(self):
1344  return self._iterates_over_iterates_over
1345 
1346  def local_vars(self):
1347  raise NotImplementedError("Kern.local_vars should be implemented")
1348 
1349  def gen_code(self, parent):
1350  raise NotImplementedError("Kern.gen_code should be implemented")
1351 
1352 
1354  '''
1355  Class representing a call to a PSyclone Kernel with a user-provided
1356  implementation. The kernel may or may not be in-lined.
1357 
1358  :param type KernelArguments: the API-specific sub-class of \
1359  :py:class:`psyclone.psyGen.Arguments` to \
1360  create.
1361  :param call: Details of the call to this kernel in the Algorithm layer.
1362  :type call: :py:class:`psyclone.parse.algorithm.KernelCall`.
1363  :param parent: the parent of this Node (kernel call) in the Schedule.
1364  :type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`.
1365  :param bool check: whether to check for consistency between the \
1366  kernel metadata and the algorithm layer. Defaults to True.
1367 
1368  '''
1369  # Textual description of the node.
1370  _text_name = "CodedKern"
1371  _colour = "magenta"
1372 
1373  def __init__(self, KernelArguments, call, parent=None, check=True):
1374  # Set module_name first in case there is an error when
1375  # processing arguments, as we can then return the module_name
1376  # from where it happened.
1377  self._module_name_module_name = call.module_name
1378  super(CodedKern, self).__init__(parent, call,
1379  call.ktype.procedure.name,
1380  KernelArguments, check)
1381  self._module_code_module_code = call.ktype._ast
1382  self._kernel_code_kernel_code = call.ktype.procedure
1383  self._fp2_ast_fp2_ast = None # The fparser2 AST for the kernel
1384  self._kern_schedule_kern_schedule = None # PSyIR schedule for the kernel
1385  # Whether or not this kernel has been transformed
1386  self._modified_modified = False
1387  # Whether or not to in-line this kernel into the module containing
1388  # the PSy layer
1389  self._module_inline_module_inline = False
1390  self._opencl_options_opencl_options = {'local_size': 64, 'queue_number': 1}
1391  self.arg_descriptorsarg_descriptorsarg_descriptorsarg_descriptors = call.ktype.arg_descriptors
1392 
1394  '''
1395  Returns a PSyIR Schedule representing the kernel code. The Schedule
1396  is just generated on first invocation, this allows us to retain
1397  transformations that may subsequently be applied to the Schedule.
1398 
1399  :returns: Schedule representing the kernel code.
1400  :rtype: :py:class:`psyclone.psyir.nodes.KernelSchedule`
1401  '''
1402  from psyclone.psyir.frontend.fparser2 import Fparser2Reader
1403  if self._kern_schedule_kern_schedule is None:
1404  astp = Fparser2Reader()
1405  self._kern_schedule_kern_schedule = astp.generate_schedule(self.namenamenamename, self.astast)
1406  # TODO: Validate kernel with metadata (issue #288).
1407  return self._kern_schedule_kern_schedule
1408 
1409  @property
1410  def opencl_options(self):
1411  '''
1412  :returns: dictionary of OpenCL options regarding the kernel.
1413  :rtype: dictionary
1414  '''
1415  return self._opencl_options_opencl_options
1416 
1417  def set_opencl_options(self, options):
1418  '''
1419  Validate and store a set of options associated with the Kernel to
1420  tune the OpenCL code generation.
1421 
1422  :param options: a set of options to tune the OpenCL code.
1423  :type options: dictionary of <string>:<value>
1424 
1425  '''
1426  valid_opencl_kernel_options = ['local_size', 'queue_number']
1427 
1428  # Validate that the options given are supported
1429  for key, value in options.items():
1430  if key in valid_opencl_kernel_options:
1431  if key == "local_size":
1432  if not isinstance(value, int):
1433  raise TypeError(
1434  "CodedKern OpenCL option 'local_size' should be "
1435  "an integer.")
1436  if key == "queue_number":
1437  if not isinstance(value, int):
1438  raise TypeError(
1439  "CodedKern OpenCL option 'queue_number' should be "
1440  "an integer.")
1441  else:
1442  raise AttributeError(
1443  f"CodedKern does not support the OpenCL option '{key}'. "
1444  f"The supported options are: "
1445  f"{valid_opencl_kernel_options}.")
1446 
1447  self._opencl_options_opencl_options[key] = value
1448 
1449  def __str__(self):
1450  return "kern call: " + self._name_name
1451 
1452  @property
1453  def module_name(self):
1454  '''
1455  :returns: The name of the Fortran module that contains this kernel
1456  :rtype: string
1457  '''
1458  return self._module_name_module_name
1459 
1460  @property
1461  def dag_name(self):
1462  '''
1463  :returns: the name to use in the DAG for this node.
1464  :rtype: str
1465  '''
1466  _, position = self._find_position(self.ancestor(Routine))
1467  return f"kernel_{self.name}_{position}"
1468 
1469  @property
1470  def module_inline(self):
1471  '''
1472  :returns: whether or not this kernel is being module-inlined.
1473  :rtype: bool
1474  '''
1475  return self._module_inline_module_inline
1476 
1477  @module_inline.setter
1478  def module_inline(self, value):
1479  '''
1480  Setter for whether or not to module-inline this kernel.
1481 
1482  :param bool value: whether or not to module-inline this kernel.
1483  '''
1484  if value is not True:
1485  raise TypeError(
1486  f"The module inline parameter only accepts the type boolean "
1487  f"'True' since module-inlining is irreversible. But found:"
1488  f" '{value}'.")
1489  # Do the same to all kernels in this invoke with the same name.
1490  # This is needed because gen_code/lowering would otherwise add
1491  # an import with the same name and shadow the module-inline routine
1492  # symbol.
1493  # TODO 1823: The transformation could have more control about this by
1494  # giving an option to specify if the module-inline applies to a
1495  # single kernel, the whole invoke or the whole algorithm.
1496  my_schedule = self.ancestor(InvokeSchedule)
1497  for kernel in my_schedule.walk(Kern):
1498  if kernel is self:
1499  self._module_inline_module_inline = value
1500  elif kernel.name == self.namenamenamename and kernel.module_inline != value:
1501  kernel.module_inline = value
1502 
1503  def node_str(self, colour=True):
1504  ''' Returns the name of this node with (optional) control codes
1505  to generate coloured output in a terminal that supports it.
1506 
1507  :param bool colour: whether or not to include colour control codes.
1508 
1509  :returns: description of this node, possibly coloured.
1510  :rtype: str
1511  '''
1512  return (self.coloured_name(colour) + " " + self.namenamenamename + "(" +
1513  self.argumentsarguments.names + ") " + "[module_inline=" +
1514  str(self._module_inline_module_inline) + "]")
1515 
1517  '''
1518  In-place replacement of CodedKern concept into language level
1519  PSyIR constructs. The CodedKern is implemented as a Call to a
1520  routine with the appropriate arguments.
1521 
1522  :returns: the lowered version of this node.
1523  :rtype: :py:class:`psyclone.psyir.node.Node`
1524 
1525  '''
1526  symtab = self.ancestor(InvokeSchedule).symbol_table
1527 
1528  if not self.module_inlinemodule_inlinemodule_inline:
1529  # If it is not module inlined then make sure we generate the kernel
1530  # file (and rename it when necessary).
1531  self.rename_and_writerename_and_write()
1532  # Then find or create the imported RoutineSymbol
1533  try:
1534  # Limit scope to this Invoke, since a kernel with the same name
1535  # may have been inlined from another invoke in the same file,
1536  # but we have it here marked as "not module-inlined"
1537  rsymbol = symtab.lookup(self._name_name, scope_limit=symtab.node)
1538  except KeyError:
1539  csymbol = symtab.find_or_create(
1540  self._module_name_module_name,
1541  symbol_type=ContainerSymbol)
1542  rsymbol = symtab.new_symbol(
1543  self._name_name,
1544  symbol_type=RoutineSymbol,
1545  # And allow shadowing in case it is also inlined with
1546  # the same name by another invoke
1547  shadowing=True,
1548  interface=ImportInterface(csymbol))
1549  else:
1550  # If its inlined, the symbol must exist
1551  try:
1552  rsymbol = self.scope.symbol_table.lookup(self._name_name)
1553  except KeyError as err:
1554  raise GenerationError(
1555  f"Cannot generate this kernel call to '{self.name}' "
1556  f"because it is marked as module-inlined but no such "
1557  f"subroutine exists in this module.") from err
1558 
1559  # Create Call to the rsymbol with the argument expressions as children
1560  # of the new node
1561  call_node = Call.create(rsymbol, self.argumentsarguments.psyir_expressions())
1562 
1563  # Swap itself with the appropriate Call node
1564  self.replace_with(call_node)
1565  return call_node
1566 
1567  def incremented_arg(self):
1568  ''' Returns the argument that has INC access. Raises a
1569  FieldNotFoundError if none is found.
1570 
1571  :rtype: str
1572  :raises FieldNotFoundError: if none is found.
1573  :returns: a Fortran argument name.
1574  '''
1575  for arg in self.argumentsarguments.args:
1576  if arg.access == AccessType.INC:
1577  return arg
1578 
1579  raise FieldNotFoundError(f"Kernel {self.name} does not have an "
1580  f"argument with "
1581  f"{AccessType.INC.api_specific_name()} "
1582  f"access")
1583 
1584  @property
1585  def ast(self):
1586  '''
1587  Generate and return the fparser2 AST of the kernel source.
1588 
1589  :returns: fparser2 AST of the Fortran file containing this kernel.
1590  :rtype: :py:class:`fparser.two.Fortran2003.Program`
1591  '''
1592  from fparser.common.readfortran import FortranStringReader
1593  from fparser.two import parser
1594  # If we've already got the AST then just return it
1595  if self._fp2_ast_fp2_ast:
1596  return self._fp2_ast_fp2_ast
1597  # Use the fparser1 AST to generate Fortran source
1598  fortran = self._module_code_module_code.tofortran()
1599  # Create an fparser2 Fortran2008 parser
1600  my_parser = parser.ParserFactory().create(std="f2008")
1601  # Parse that Fortran using our parser
1602  reader = FortranStringReader(fortran)
1603  self._fp2_ast_fp2_ast = my_parser(reader)
1604  return self._fp2_ast_fp2_ast
1605 
1606  @staticmethod
1607  def _new_name(original, tag, suffix):
1608  '''
1609  Construct a new name given the original, a tag and a suffix (which
1610  may or may not terminate the original name). If suffix is present
1611  in the original name then the `tag` is inserted before it.
1612 
1613  :param str original: The original name
1614  :param str tag: Tag to insert into new name
1615  :param str suffix: Suffix with which to end new name.
1616  :returns: New name made of original + tag + suffix
1617  :rtype: str
1618  '''
1619  if original.endswith(suffix):
1620  return original[:-len(suffix)] + tag + suffix
1621  return original + tag + suffix
1622 
1623  def rename_and_write(self):
1624  '''
1625  Writes the (transformed) AST of this kernel to file and resets the
1626  'modified' flag to False. By default (config.kernel_naming ==
1627  "multiple"), the kernel is re-named so as to be unique within
1628  the kernel output directory stored within the configuration
1629  object. Alternatively, if config.kernel_naming is "single"
1630  then no re-naming and output is performed if there is already
1631  a transformed copy of the kernel in the output dir. (In this
1632  case a check is performed that the transformed kernel already
1633  present is identical to the one that we would otherwise write
1634  to file. If this is not the case then we raise a GenerationError.)
1635 
1636  :raises GenerationError: if config.kernel_naming == "single" and a \
1637  different, transformed version of this \
1638  kernel is already in the output directory.
1639  :raises NotImplementedError: if the kernel has been transformed but \
1640  is also flagged for module-inlining.
1641 
1642  '''
1643  from psyclone.line_length import FortLineLength
1644 
1645  config = Config.get()
1646 
1647  # If this kernel has not been transformed we do nothing, also if the
1648  # kernel has been module-inlined, the routine already exist in the
1649  # PSyIR and we don't need to generate a new file with it.
1650  if not self.modifiedmodifiedmodifiedmodified or self.module_inlinemodule_inlinemodule_inline:
1651  return
1652 
1653  # Remove any "_mod" if the file follows the PSyclone naming convention
1654  orig_mod_name = self.module_namemodule_name[:]
1655  if orig_mod_name.lower().endswith("_mod"):
1656  old_base_name = orig_mod_name[:-4]
1657  else:
1658  old_base_name = orig_mod_name[:]
1659 
1660  # We could create a hash of a string built from the name of the
1661  # Algorithm (module), the name/position of the Invoke and the
1662  # index of this kernel within that Invoke. However, that creates
1663  # a very long name so we simply ensure that kernel names are unique
1664  # within the user-supplied kernel-output directory.
1665  name_idx = -1
1666  fdesc = None
1667  while not fdesc:
1668  name_idx += 1
1669  new_suffix = ""
1670 
1671  new_suffix += f"_{name_idx}"
1672  new_name = old_base_name + new_suffix + "_mod.f90"
1673 
1674  try:
1675  # Atomically attempt to open the new kernel file (in case
1676  # this is part of a parallel build)
1677  fdesc = os.open(
1678  os.path.join(config.kernel_output_dir, new_name),
1679  os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1680  except (OSError, IOError):
1681  # The os.O_CREATE and os.O_EXCL flags in combination mean
1682  # that open() raises an error if the file exists
1683  if config.kernel_naming == "single":
1684  # If the kernel-renaming scheme is such that we only ever
1685  # create one copy of a transformed kernel then we're done
1686  break
1687  continue
1688 
1689  # Use the suffix we have determined to rename all relevant quantities
1690  # within the AST of the kernel code.
1691  self._rename_psyir_rename_psyir(new_suffix)
1692 
1693  # Kernel is now self-consistent so unset the modified flag
1694  self.modifiedmodifiedmodifiedmodified = False
1695 
1696  # If we reach this point the kernel needs to be written out into a
1697  # file using a PSyIR back-end. At the moment there is no way to choose
1698  # which back-end to use, so simply use the Fortran one (and limit the
1699  # line length).
1700  fortran_writer = FortranWriter(
1701  check_global_constraints=config.backend_checks_enabled)
1702  # Start from the root of the schedule as we want to output
1703  # any module information surrounding the kernel subroutine
1704  # as well as the subroutine itself.
1705  new_kern_code = fortran_writer(self.get_kernel_scheduleget_kernel_schedule().root)
1706  fll = FortLineLength()
1707  new_kern_code = fll.process(new_kern_code)
1708 
1709  if not fdesc:
1710  # If we've not got a file descriptor at this point then that's
1711  # because the file already exists and the kernel-naming scheme
1712  # ("single") means we're not creating a new one.
1713  # Check that what we've got is the same as what's in the file
1714  with open(os.path.join(config.kernel_output_dir,
1715  new_name), "r") as ffile:
1716  kern_code = ffile.read()
1717  if kern_code != new_kern_code:
1718  raise GenerationError(
1719  f"A transformed version of this Kernel "
1720  f"'{self._module_name + '''.f90'''}' already exists "
1721  f"in the kernel-output directory "
1722  f"({config.kernel_output_dir}) but is not the "
1723  f"same as the current, transformed kernel and the "
1724  f"kernel-renaming scheme is set to "
1725  f"'{config.kernel_naming}'. (If you wish to"
1726  f" generate a new, unique kernel for every kernel "
1727  f"that is transformed then use "
1728  f"'--kernel-renaming multiple'.)")
1729  else:
1730  # Write the modified AST out to file
1731  os.write(fdesc, new_kern_code.encode())
1732  # Close the new kernel file
1733  os.close(fdesc)
1734 
1735  def _rename_psyir(self, suffix):
1736  '''Rename the PSyIR module and kernel names by adding the supplied
1737  suffix to the names. This change affects the KernCall and
1738  KernelSchedule nodes as well as the kernel metadata declaration.
1739 
1740  :param str suffix: the string to insert into the quantity names.
1741 
1742  '''
1743  # We need to get the kernel schedule before modifying self.name
1744  kern_schedule = self.get_kernel_scheduleget_kernel_schedule()
1745  container = kern_schedule.ancestor(Container)
1746 
1747  # Use the suffix to create a new kernel name. This will
1748  # conform to the PSyclone convention of ending in "_code"
1749  orig_mod_name = self.module_namemodule_name[:]
1750  orig_kern_name = self.namenamenamename[:]
1751 
1752  new_kern_name = self._new_name_new_name(orig_kern_name, suffix, "_code")
1753  new_mod_name = self._new_name_new_name(orig_mod_name, suffix, "_mod")
1754 
1755  # Change the name of this kernel and the associated
1756  # module. These names are used when generating the PSy-layer.
1757  self.namenamenamename = new_kern_name[:]
1758  self._module_name_module_name = new_mod_name[:]
1759  kern_schedule.name = new_kern_name[:]
1760  container.name = new_mod_name[:]
1761 
1762  # Change the name of the symbol
1763  try:
1764  kern_symbol = kern_schedule.symbol_table.lookup(orig_kern_name)
1765  container.symbol_table.rename_symbol(kern_symbol, new_kern_name)
1766  except KeyError:
1767  # TODO #1013. Right now not all tests have PSyIR symbols because
1768  # some only expect f2pygen generation.
1769  pass
1770 
1771  # Ensure the metadata points to the correct procedure now. Since this
1772  # routine is general purpose, we won't always have a domain-specific
1773  # Container here and if we don't, it won't have a 'metadata' property.
1774  if hasattr(container, "metadata"):
1775  container.metadata.procedure_name = new_kern_name[:]
1776  # TODO #928 - until the LFRic KernelInterface is fully functional, we
1777  # can't raise language-level PSyIR to LFRic and therefore we have to
1778  # manually fix the name of the procedure within the text that stores
1779  # the kernel metadata.
1780  container_table = container.symbol_table
1781  for sym in container_table.datatypesymbols:
1782  if isinstance(sym.datatype, UnsupportedFortranType):
1783  new_declaration = sym.datatype.declaration.replace(
1784  orig_kern_name, new_kern_name)
1785  # pylint: disable=protected-access
1786  sym._datatype = UnsupportedFortranType(
1787  new_declaration,
1788  partial_datatype=sym.datatype.partial_datatype)
1789  # pylint: enable=protected-access
1790 
1791  @property
1792  def modified(self):
1793  '''
1794  :returns: Whether or not this kernel has been modified (transformed).
1795  :rtype: bool
1796  '''
1797  return self._modified_modified
1798 
1799  @modified.setter
1800  def modified(self, value):
1801  '''
1802  Setter for whether or not this kernel has been modified.
1803 
1804  :param bool value: True if kernel modified, False otherwise.
1805  '''
1806  self._modified_modified = value
1807 
1808 
1810  '''A class representing a kernel that is inlined.
1811  It has one child which stores the Schedule for the child nodes.
1812 
1813  :param psyir_nodes: the list of PSyIR nodes that represent the body
1814  of this kernel.
1815  :type psyir_nodes: list of :py:class:`psyclone.psyir.nodes.Node`
1816  :param parent: the parent of this node in the PSyIR.
1817  :type parent: sub-class of :py:class:`psyclone.psyir.nodes.Node`
1818  '''
1819  # Textual description of the node.
1820  _children_valid_format = "Schedule"
1821  _text_name = "InlinedKern"
1822  _colour = "magenta"
1823 
1824  def __init__(self, psyir_nodes, parent=None):
1825  # pylint: disable=non-parent-init-called, super-init-not-called
1826  Node.__init__(self, parent=parent)
1827  schedule = Schedule(children=psyir_nodes, parent=self)
1828  self.childrenchildren = [schedule]
1829  self._arguments_arguments_arguments = None
1830 
1831  @staticmethod
1832  def _validate_child(position, child):
1833  '''
1834  :param int position: the position to be validated.
1835  :param child: a child to be validated.
1836  :type child: :py:class:`psyclone.psyir.nodes.Node`
1837 
1838  :return: whether the given child and position are valid for this node.
1839  :rtype: bool
1840 
1841  '''
1842  return position == 0 and isinstance(child, Schedule)
1843 
1844  @abc.abstractmethod
1845  def local_vars(self):
1846  '''
1847  :returns: list of the variable (names) that are local to this kernel \
1848  (and must therefore be e.g. threadprivate if doing OpenMP)
1849  :rtype: list of str
1850  '''
1851 
1852  def node_str(self, colour=True):
1853  ''' Returns the name of this node with (optional) control codes
1854  to generate coloured output in a terminal that supports it.
1855 
1856  :param bool colour: whether or not to include colour control codes.
1857 
1858  :returns: description of this node, possibly coloured.
1859  :rtype: str
1860  '''
1861  return self.coloured_name(colour) + "[]"
1862 
1863 
1864 class BuiltIn(Kern):
1865  '''
1866  Parent class for all built-ins (field operations for which the user
1867  does not have to provide an implementation).
1868  '''
1869  # Textual description of the node.
1870  _text_name = "BuiltIn"
1871  _colour = "magenta"
1872 
1873  def __init__(self):
1874  # We cannot call Kern.__init__ as don't have necessary information
1875  # here. Instead we provide a load() method that can be called once
1876  # that information is available.
1877  self._arg_descriptors_arg_descriptors_arg_descriptors = None
1878  self._func_descriptors_func_descriptors = None
1879  self._fs_descriptors_fs_descriptors = None
1880  self._reduction_reduction_reduction = None
1881 
1882  @property
1883  def dag_name(self):
1884  '''
1885  :returns: the name to use in the DAG for this node.
1886  :rtype: str
1887  '''
1888  _, position = self._find_position(self.ancestor(Routine))
1889  return f"builtin_{self.name}_{position}"
1890 
1891  def load(self, call, arguments, parent=None):
1892  ''' Set-up the state of this BuiltIn call '''
1893  name = call.ktype.name
1894  super(BuiltIn, self).__init__(parent, call, name, arguments)
1895 
1896  def local_vars(self):
1897  '''Variables that are local to this built-in and therefore need to be
1898  made private when parallelising using OpenMP or similar. By default
1899  builtin's do not have any local variables so set to nothing'''
1900  return []
1901 
1902 
1903 class Arguments():
1904  '''
1905  Arguments abstract base class.
1906 
1907  :param parent_call: kernel call with which the arguments are associated.
1908  :type parent_call: sub-class of :py:class:`psyclone.psyGen.Kern`
1909  '''
1910  def __init__(self, parent_call):
1911  # TODO #2503: This reference is not kept updated when copign the
1912  # parent
1913  self._parent_call_parent_call = parent_call
1914  # The container object holding information on all arguments
1915  # (derived from both kernel meta-data and the kernel call
1916  # in the Algorithm layer).
1917  self._args_args = []
1918 
1919  @abc.abstractmethod
1921  '''
1922  :returns: the PSyIR expressions representing this Argument list.
1923  :rtype: list of :py:class:`psyclone.psyir.nodes.Node`
1924 
1925  '''
1926 
1927  @property
1928  def names(self):
1929  '''
1930  :returns: the Algorithm-visible kernel arguments in a \
1931  comma-delimited string.
1932  :rtype: str
1933  '''
1934  return ",".join([arg.name for arg in self.argsargs])
1935 
1936  @property
1937  def args(self):
1938  return self._args_args
1939 
1941  '''
1942  Returns an argument that can be iterated over, i.e. modified
1943  (has WRITE, READWRITE or INC access), but not the result of
1944  a reduction operation.
1945 
1946  :returns: a Fortran argument name
1947  :rtype: string
1948  :raises GenerationError: if none such argument is found.
1949 
1950  '''
1951  for arg in self._args_args:
1952  if arg.access in AccessType.all_write_accesses() and \
1953  arg.access not in AccessType.get_valid_reduction_modes():
1954  return arg
1955  raise GenerationError("psyGen:Arguments:iteration_space_arg Error, "
1956  "we assume there is at least one writer, "
1957  "reader/writer, or increment as an argument")
1958 
1959  @property
1960  def acc_args(self):
1961  '''
1962  :returns: the list of quantities that must be available on an \
1963  OpenACC device before the associated kernel can be launched
1964  :rtype: list of str
1965  '''
1966  raise NotImplementedError(
1967  "Arguments.acc_args must be implemented in sub-class")
1968 
1969  @property
1970  def scalars(self):
1971  '''
1972  :returns: the list of scalar quantities belonging to this object
1973  :rtype: list of str
1974  '''
1975  raise NotImplementedError(
1976  "Arguments.scalars must be implemented in sub-class")
1977 
1978  def append(self, name, argument_type):
1979  ''' Abstract method to append KernelArguments to the Argument
1980  list.
1981 
1982  :param str name: name of the appended argument.
1983  :param str argument_type: type of the appended argument.
1984  '''
1985  raise NotImplementedError(
1986  "Arguments.append must be implemented in sub-class")
1987 
1988 
1989 class DataAccess():
1990  '''A helper class to simplify the determination of dependencies due to
1991  overlapping accesses to data associated with instances of the
1992  Argument class.
1993 
1994  '''
1995 
1996  def __init__(self, arg):
1997  '''Store the argument associated with the instance of this class and
1998  the Call, HaloExchange or GlobalSum (or a subclass thereof)
1999  instance with which the argument is associated.
2000 
2001  :param arg: the argument that we are concerned with. An \
2002  argument can be found in a `Kern` a `HaloExchange` or a \
2003  `GlobalSum` (or a subclass thereof)
2004  :type arg: :py:class:`psyclone.psyGen.Argument`
2005 
2006  '''
2007  # the `psyclone.psyGen.Argument` we are concerned with
2008  self._arg_arg = arg
2009  # The call (Kern, HaloExchange, GlobalSum or subclass)
2010  # instance with which the argument is associated
2011  self._call_call = arg.call
2012  # initialise _covered and _vector_index_access to keep pylint
2013  # happy
2014  self._covered_covered = None
2015  self._vector_index_access_vector_index_access = None
2016  # Now actually set them to the required initial values
2017  self.reset_coveragereset_coverage()
2018 
2019  def overlaps(self, arg):
2020  '''Determine whether the accesses to the provided argument overlap
2021  with the accesses of the source argument. Overlap means that
2022  the accesses share at least one memory location. For example,
2023  the arguments both access the 1st index of the same field.
2024 
2025  We do not currently deal with accesses to a subset of an
2026  argument (unless it is a vector). This distinction will need
2027  to be added once loop splitting is supported.
2028 
2029  :param arg: the argument to compare with our internal argument
2030  :type arg: :py:class:`psyclone.psyGen.Argument`
2031  :return bool: True if there are overlapping accesses between \
2032  arguments (i.e. accesses share at least one memory \
2033  location) and False if not.
2034 
2035  '''
2036  if self._arg_arg.name != arg.name:
2037  # the arguments are different args so do not overlap
2038  return False
2039 
2040  if isinstance(self._call_call, HaloExchange) and \
2041  isinstance(arg.call, HaloExchange) and \
2042  (self._arg_arg.vector_size > 1 or arg.vector_size > 1):
2043  # This is a vector field and both accesses come from halo
2044  # exchanges. As halo exchanges only access a particular
2045  # vector, the accesses do not overlap if the vector indices
2046  # being accessed differ.
2047 
2048  # sanity check
2049  if self._arg_arg.vector_size != arg.vector_size:
2050  raise InternalError(
2051  f"DataAccess.overlaps(): vector sizes differ for field "
2052  f"'{arg.name}' in two halo exchange calls. Found "
2053  f"'{self._arg.vector_size}' and '{arg.vector_size}'")
2054  if self._call_call.vector_index != arg.call.vector_index:
2055  # accesses are to different vector indices so do not overlap
2056  return False
2057  # accesses do overlap
2058  return True
2059 
2060  def reset_coverage(self):
2061  '''Reset internal state to allow re-use of the object for a different
2062  situation.
2063 
2064  '''
2065  # False unless all data accessed by our local argument has
2066  # also been accessed by other arguments.
2067  self._covered_covered = False
2068  # Used to store individual vector component accesses when
2069  # checking that all vector components have been accessed.
2070  self._vector_index_access_vector_index_access = []
2071 
2072  def update_coverage(self, arg):
2073  '''Record any overlap between accesses to the supplied argument and
2074  the internal argument. Overlap means that the accesses to the
2075  two arguments share at least one memory location. If the
2076  overlap results in all of the accesses to the internal
2077  argument being covered (either directly or as a combination
2078  with previous arguments) then ensure that the covered() method
2079  returns True. Covered means that all memory accesses by the
2080  internal argument have at least one corresponding access by
2081  the supplied arguments.
2082 
2083  :param arg: the argument used to compare with our internal \
2084  argument in order to update coverage information
2085  :type arg: :py:class:`psyclone.psyGen.Argument`
2086 
2087  '''
2088 
2089  if not self.overlapsoverlaps(arg):
2090  # There is no overlap so there is nothing to update.
2091  return
2092 
2093  if isinstance(arg.call, HaloExchange) and \
2094  (hasattr(self._arg_arg, 'vector_size') and self._arg_arg.vector_size > 1):
2095  # The supplied argument is a vector field coming from a
2096  # halo exchange and therefore only accesses one of the
2097  # vectors
2098 
2099  if isinstance(self._call_call, HaloExchange):
2100  # I am also a halo exchange so only access one of the
2101  # vectors. At this point the vector indices of the two
2102  # halo exchange fields must be the same, which should
2103  # never happen due to checks in the `overlaps()`
2104  # method earlier
2105  raise InternalError(
2106  f"DataAccess:update_coverage() The halo exchange vector "
2107  f"indices for '{self._arg.name}' are the same. This "
2108  f"should never happen")
2109  else:
2110  # I am not a halo exchange so access all components of
2111  # the vector. However, the supplied argument is a halo
2112  # exchange so only accesses one of the
2113  # components. This results in partial coverage
2114  # (i.e. the overlap in accesses is partial). Therefore
2115  # record the index that is accessed and check whether
2116  # all indices are now covered (which would mean `full`
2117  # coverage).
2118  if arg.call.vector_index in self._vector_index_access_vector_index_access:
2119  raise InternalError(
2120  "DataAccess:update_coverage() Found more than one "
2121  "dependent halo exchange with the same vector index")
2122  self._vector_index_access_vector_index_access.append(arg.call.vector_index)
2123  if len(self._vector_index_access_vector_index_access) != self._arg_arg.vector_size:
2124  return
2125  # This argument is covered i.e. all accesses by the
2126  # internal argument have a corresponding access in one of the
2127  # supplied arguments.
2128  self._covered_covered = True
2129 
2130  @property
2131  def covered(self):
2132  '''Returns True if all of the data associated with this argument has
2133  been covered by the arguments provided in update_coverage
2134 
2135  :return bool: True if all of an argument is covered by \
2136  previous accesses and False if not.
2137 
2138  '''
2139  return self._covered_covered
2140 
2141 
2142 class Argument():
2143  '''
2144  Argument base class. Captures information on an argument that is passed
2145  to a Kernel from an Invoke.
2146 
2147  :param call: the kernel call that this argument is associated with.
2148  :type call: :py:class:`psyclone.psyGen.Kern`
2149  :param arg_info: Information about this argument collected by \
2150  the parser.
2151  :type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
2152  :param access: the way in which this argument is accessed in \
2153  the 'Kern'. Valid values are specified in the config \
2154  object of the current API.
2155  :type access: str
2156 
2157  '''
2158  # pylint: disable=too-many-instance-attributes
2159  def __init__(self, call, arg_info, access):
2160  self._call_call = call
2161  if arg_info is not None:
2162  self._text_text = arg_info.text
2163  self._orig_name_orig_name = arg_info.varname
2164  self._form_form = arg_info.form
2165  self._is_literal_is_literal = arg_info.is_literal()
2166  else:
2167  self._text_text = ""
2168  self._orig_name_orig_name = ""
2169  self._form_form = ""
2170  self._is_literal_is_literal = False
2171  # Initialise access
2172  self._access_access = access
2173  # Default the precision, data type and module to 'None' (no
2174  # explicit property specified)
2175  self._precision_precision = None
2176  self._data_type_data_type = None
2177  self._module_name_module_name = None
2178  # Default the name to the original name for debugging
2179  # purposes. This may be updated when _complete_init() is
2180  # called.
2181  self._name_name = self._orig_name_orig_name
2182 
2183  def _complete_init(self, arg_info):
2184  '''Provides the initialisation of name, text and the declaration of
2185  symbols in the symbol table if required. This initialisation
2186  is not performed in the constructor as subclasses may need to
2187  perform additional initialisation before infer_datatype is
2188  called (in order to determine the values of precision,
2189  data_type and module_name).
2190 
2191  :param arg_info: Information about this argument collected by \
2192  the parser.
2193  :type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
2194 
2195  '''
2196  if self._orig_name_orig_name is None:
2197  # this is an infrastructure call literal argument. Therefore
2198  # we do not want an argument (_text=None) but we do want to
2199  # keep the value (_name)
2200  self._name_name = arg_info.text
2201  self._text_text = None
2202  else:
2203  # There are unit-tests where we create Arguments without an
2204  # associated call or InvokeSchedule.
2205  if self._call_call and self._call_call.ancestor(InvokeSchedule):
2206  symtab = self._call_call.ancestor(InvokeSchedule).symbol_table
2207 
2208  # Keep original list of arguments
2209  previous_arguments = symtab.argument_list
2210 
2211  # Find the tag to use
2212  tag = "AlgArgs_" + self._text_text
2213 
2214  # Prepare the Argument Interface Access value
2215  argument_access = ArgumentInterface.Access.READWRITE
2216 
2217  # Find the tag or create a new symbol with expected attributes
2218  data_type = self.infer_datatypeinfer_datatype()
2219  # In case of LFRic field vector, declare it as array.
2220  # This is a fix for #1930, but we might want a better
2221  # solution to avoid LFRic-specific code here.
2222  # pylint: disable=no-member
2223  if hasattr(self, 'vector_size') and self.vector_size > 1:
2224  data_type = ArrayType(data_type, [self.vector_size])
2225 
2226  new_argument = symtab.find_or_create_tag(
2227  tag, root_name=self._orig_name_orig_name, symbol_type=DataSymbol,
2228  datatype=data_type,
2229  interface=ArgumentInterface(argument_access))
2230  self._name_name = new_argument.name
2231 
2232  # Unless the argument already exists with another interface
2233  # (e.g. import) they come from the invoke argument list
2234  if (isinstance(new_argument.interface, ArgumentInterface) and
2235  new_argument not in previous_arguments):
2236  symtab.specify_argument_list(previous_arguments +
2237  [new_argument])
2238 
2239  @abc.abstractmethod
2240  def psyir_expression(self):
2241  '''
2242  :returns: the PSyIR expression represented by this Argument.
2243  :rtype: :py:class:`psyclone.psyir.nodes.Node`
2244 
2245  '''
2246 
2247  def infer_datatype(self):
2248  ''' Infer the datatype of this argument using the API rules. If no
2249  specialisation of this method has been provided make the type
2250  UnresolvedType for now (it may be provided later in the execution).
2251 
2252  :returns: the datatype of this argument.
2253  :rtype: :py:class::`psyclone.psyir.symbols.DataType`
2254 
2255  '''
2256  return UnresolvedType()
2257 
2258  def __str__(self):
2259  return self._name
2260 
2261  @property
2262  def name(self):
2263  return self._name
2264 
2265  @property
2266  def text(self):
2267  return self._text
2268 
2269  @property
2270  def form(self):
2271  return self._form
2272 
2273  @property
2274  def is_literal(self):
2275  return self._is_literal
2276 
2277  @property
2278  def access(self):
2279  return self._access
2280 
2281  @access.setter
2282  def access(self, value):
2283  '''Set the access type for this argument.
2284 
2285  :param value: new access type.
2286  :type value: :py:class:`psyclone.core.access_type.AccessType`.
2287 
2288  :raises InternalError: if value is not an AccessType.
2289 
2290  '''
2291  if not isinstance(value, AccessType):
2292  raise InternalError(f"Invalid access type '{value}' of type "
2293  f"'{type(value)}.")
2294 
2295  self._access_access = value
2296 
2297  @property
2298  def argument_type(self):
2299  '''
2300  Returns the type of the argument. APIs that do not have this
2301  concept can use this base class version which just returns "field"
2302  in all cases. APIs with this concept can override this method.
2303 
2304  :returns: the API type of the kernel argument.
2305  :rtype: str
2306 
2307  '''
2308  return "field"
2309 
2310  @property
2311  @abc.abstractmethod
2312  def intrinsic_type(self):
2313  '''
2314  Abstract property for the intrinsic type of the argument with
2315  specific implementations in different APIs.
2316 
2317  :returns: the intrinsic type of this argument.
2318  :rtype: str
2319 
2320  '''
2321 
2322  @property
2323  def precision(self):
2324  '''
2325  :returns: the precision of this argument. Default value is None, \
2326  explicit implementation is left to a specific API.
2327  :rtype: str or NoneType
2328 
2329  '''
2330  return self._precision_precision
2331 
2332  @property
2333  def data_type(self):
2334  '''
2335  :returns: the data type of this argument. Default value is None, \
2336  explicit implementation is left to a specific API.
2337  :rtype: str or NoneType
2338 
2339  '''
2340  return self._data_type_data_type
2341 
2342  @property
2343  def module_name(self):
2344  '''
2345  :returns: the name of the Fortran module that contains definitions \
2346  for the argument data type. Default value is None, \
2347  explicit implementation is left to a specific API.
2348  :rtype: str or NoneType
2349 
2350 
2351  '''
2352  return self._module_name_module_name
2353 
2354  @property
2355  def call(self):
2356  ''' Return the call that this argument is associated with '''
2357  return self._call_call
2358 
2359  @call.setter
2360  def call(self, value):
2361  ''' set the node that this argument is associated with '''
2362  self._call_call = value
2363 
2365  '''Returns the preceding argument that this argument has a direct
2366  dependence with, or None if there is not one. The argument may
2367  exist in a call, a haloexchange, or a globalsum.
2368 
2369  :returns: the first preceding argument that has a dependence \
2370  on this argument.
2371  :rtype: :py:class:`psyclone.psyGen.Argument`
2372 
2373  '''
2374  nodes = self._call_call.preceding(reverse=True)
2375  return self._find_argument_find_argument(nodes)
2376 
2377  def forward_write_dependencies(self, ignore_halos=False):
2378  '''Returns a list of following write arguments that this argument has
2379  dependencies with. The arguments may exist in a call, a
2380  haloexchange (unless `ignore_halos` is `True`), or a globalsum. If
2381  none are found then return an empty list. If self is not a
2382  reader then return an empty list.
2383 
2384  :param bool ignore_halos: if `True` then any write dependencies \
2385  involving a halo exchange are ignored. Defaults to `False`.
2386 
2387  :returns: a list of arguments that have a following write \
2388  dependence on this argument.
2389  :rtype: list of :py:class:`psyclone.psyGen.Argument`
2390 
2391  '''
2392  nodes = self._call_call.following()
2393  results = self._find_write_arguments_find_write_arguments(nodes, ignore_halos=ignore_halos)
2394  return results
2395 
2396  def backward_write_dependencies(self, ignore_halos=False):
2397  '''Returns a list of previous write arguments that this argument has
2398  dependencies with. The arguments may exist in a call, a
2399  haloexchange (unless `ignore_halos` is `True`), or a globalsum. If
2400  none are found then return an empty list. If self is not a
2401  reader then return an empty list.
2402 
2403  :param ignore_halos: if `True` then any write dependencies \
2404  involving a halo exchange are ignored. Defaults to `False`.
2405  :type ignore_halos: bool
2406 
2407  :returns: a list of arguments that have a preceding write \
2408  dependence on this argument.
2409  :rtype: list of :py:class:`psyclone.psyGen.Argument`
2410 
2411  '''
2412  nodes = self._call_call.preceding(reverse=True)
2413  results = self._find_write_arguments_find_write_arguments(nodes, ignore_halos=ignore_halos)
2414  return results
2415 
2417  '''Returns the following argument that this argument has a direct
2418  dependence on, or `None` if there is not one. The argument may
2419  exist in a call, a haloexchange, or a globalsum.
2420 
2421  :returns: the first following argument that has a dependence \
2422  on this argument.
2423  :rtype: :py:class:`psyclone.psyGen.Argument`
2424 
2425  '''
2426  nodes = self._call_call.following()
2427  return self._find_argument_find_argument(nodes)
2428 
2430  '''Returns a list of following read arguments that this argument has
2431  dependencies with. The arguments may exist in a call, a
2432  haloexchange, or a globalsum. If none are found then
2433  return an empty list. If self is not a writer then return an
2434  empty list.
2435 
2436  :returns: a list of following arguments that have a read \
2437  dependence on this argument.
2438  :rtype: list of :py:class:`psyclone.psyGen.Argument`
2439 
2440  '''
2441  nodes = self._call_call.following()
2442  return self._find_read_arguments_find_read_arguments(nodes)
2443 
2444  def _find_argument(self, nodes):
2445  '''Return the first argument in the list of nodes that has a
2446  dependency with self. If one is not found return None
2447 
2448  :param nodes: the list of nodes that this method examines.
2449  :type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
2450 
2451  :returns: An argument object or None.
2452  :rtype: :py:class:`psyclone.psyGen.Argument`
2453 
2454  '''
2455  nodes_with_args = [x for x in nodes if
2456  isinstance(x, (Kern, HaloExchange, GlobalSum))]
2457  for node in nodes_with_args:
2458  for argument in node.args:
2459  if self._depends_on_depends_on(argument):
2460  return argument
2461  return None
2462 
2463  def _find_read_arguments(self, nodes):
2464  '''Return a list of arguments from the list of nodes that have a read
2465  dependency with self. If none are found then return an empty
2466  list. If self is not a writer then return an empty list.
2467 
2468  :param nodes: the list of nodes that this method examines.
2469  :type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
2470 
2471  :returns: a list of arguments that have a read dependence on \
2472  this argument.
2473  :rtype: list of :py:class:`psyclone.psyGen.Argument`
2474 
2475  '''
2476  if self.accessaccessaccess not in AccessType.all_write_accesses():
2477  # I am not a writer so there will be no read dependencies
2478  return []
2479 
2480  # We only need consider nodes that have arguments
2481  nodes_with_args = [x for x in nodes if
2482  isinstance(x, (Kern, HaloExchange, GlobalSum))]
2483  access = DataAccess(self)
2484  arguments = []
2485  for node in nodes_with_args:
2486  for argument in node.args:
2487  # look at all arguments in our nodes
2488  if argument.access in AccessType.all_read_accesses() and \
2489  access.overlaps(argument):
2490  arguments.append(argument)
2491  if argument.access in AccessType.all_write_accesses():
2492  access.update_coverage(argument)
2493  if access.covered:
2494  # We have now found all arguments upon which
2495  # this argument depends so return the list.
2496  return arguments
2497 
2498  # we did not find a terminating write dependence in the list
2499  # of nodes so we return any read dependencies that were found
2500  return arguments
2501 
2502  def _find_write_arguments(self, nodes, ignore_halos=False):
2503  '''Return a list of arguments from the list of nodes that have a write
2504  dependency with self. If none are found then return an empty
2505  list. If self is not a reader then return an empty list.
2506 
2507  :param nodes: the list of nodes that this method examines.
2508  :type nodes: list of :py:class:`psyclone.psyir.nodes.Node`
2509 
2510  :param bool ignore_halos: if `True` then any write dependencies \
2511  involving a halo exchange are ignored. Defaults to `False`.
2512  :returns: a list of arguments that have a write dependence with \
2513  this argument.
2514  :rtype: list of :py:class:`psyclone.psyGen.Argument`
2515 
2516  '''
2517  if self.accessaccessaccess not in AccessType.all_read_accesses():
2518  # I am not a reader so there will be no write dependencies
2519  return []
2520 
2521  # We only need consider nodes that have arguments
2522  nodes_with_args = [x for x in nodes if
2523  isinstance(x, (Kern, GlobalSum)) or
2524  (isinstance(x, HaloExchange) and not ignore_halos)]
2525  access = DataAccess(self)
2526  arguments = []
2527  for node in nodes_with_args:
2528  for argument in node.args:
2529  # look at all arguments in our nodes
2530  if argument.access not in AccessType.all_write_accesses():
2531  # no dependence if not a writer
2532  continue
2533  if not access.overlaps(argument):
2534  # Accesses are independent of each other
2535  continue
2536  arguments.append(argument)
2537  access.update_coverage(argument)
2538  if access.covered:
2539  # sanity check
2540  if not isinstance(node, HaloExchange) and \
2541  len(arguments) > 1:
2542  raise InternalError(
2543  "Found a writer dependence but there are already "
2544  "dependencies. This should not happen.")
2545  # We have now found all arguments upon which this
2546  # argument depends so return the list.
2547  return arguments
2548  if arguments:
2549  raise InternalError(
2550  "Argument()._field_write_arguments() There are no more nodes "
2551  "but there are already dependencies. This should not happen.")
2552  # no dependencies have been found
2553  return []
2554 
2555  def _depends_on(self, argument):
2556  '''If there is a dependency between the argument and self then return
2557  True, otherwise return False. We consider there to be a
2558  dependency between two arguments if the names are the same and
2559  if one reads and one writes, or if both write. Dependencies
2560  are often defined as being read-after-write (RAW),
2561  write-after-read (WAR) and write after write (WAW). These
2562  dependencies can be considered to be forward dependencies, in
2563  the sense that RAW means that the read is after the write in
2564  the schedule. Similarly for WAR and WAW. We capture these
2565  dependencies in this method. However we also capture
2566  dependencies in the opposite direction (backward
2567  dependencies). These are the same dependencies as forward
2568  dependencies but are reversed. One could consider these to be
2569  read-before-write, write-before-read, and
2570  write-before-write. The terminology of forward and backward to
2571  indicate whether the argument we depend on is after or before
2572  us in the schedule is borrowed from loop dependence analysis
2573  where a forward dependence indicates a dependence in a future
2574  loop iteration and a backward dependence indicates a
2575  dependence on a previous loop iteration. Note, we currently
2576  assume that any read or write to an argument results in a
2577  dependence i.e. we do not consider the internal structure of
2578  the argument (e.g. it may be an array). However, this
2579  assumption is OK as all elements of an array are typically
2580  accessed. However, we may need to revisit this when we change
2581  the iteration spaces of loops e.g. for overlapping
2582  communication and computation.
2583 
2584  :param argument: the argument we will check to see whether \
2585  there is a dependence on this argument instance (self).
2586  :type argument: :py:class:`psyclone.psyGen.Argument`
2587 
2588  :returns: True if there is a dependence and False if not.
2589  :rtype: bool
2590 
2591  '''
2592  if argument.name == self._name_name:
2593  if self.accessaccessaccess in AccessType.all_write_accesses() and \
2594  argument.access in AccessType.all_read_accesses():
2595  return True
2596  if self.accessaccessaccess in AccessType.all_read_accesses() and \
2597  argument.access in AccessType.all_write_accesses():
2598  return True
2599  if self.accessaccessaccess in AccessType.all_write_accesses() and \
2600  argument.access in AccessType.all_write_accesses():
2601  return True
2602  return False
2603 
2604 
2606  '''
2607  This class provides information about individual kernel-call
2608  arguments as specified by the kernel argument metadata and the
2609  kernel invocation in the Algorithm layer.
2610 
2611  :param arg: information obtained from the metadata for this kernel \
2612  argument.
2613  :type arg: :py:class:`psyclone.parse.kernel.Descriptor`
2614  :param arg_info: information on how this argument is specified in \
2615  the Algorithm layer.
2616  :type arg_info: :py:class:`psyclone.parse.algorithm.Arg`
2617  :param call: the PSyIR kernel node to which this argument pertains.
2618  :type call: :py:class:`psyclone.psyGen.Kern`
2619 
2620  '''
2621  def __init__(self, arg, arg_info, call):
2622  self._arg_arg = arg
2623  super().__init__(call, arg_info, arg.access)
2624 
2625  @property
2626  def space(self):
2627  return self._arg_arg.function_space
2628 
2629  @property
2630  def stencil(self):
2631  return self._arg_arg.stencil
2632 
2633  @property
2634  @abc.abstractmethod
2635  def is_scalar(self):
2636  ''':returns: whether this variable is a scalar variable or not.
2637  :rtype: bool'''
2638 
2639  @property
2640  def metadata_index(self):
2641  '''
2642  :returns: the position of the corresponding argument descriptor in \
2643  the kernel metadata.
2644  :rtype: int
2645  '''
2646  return self._arg_arg.metadata_index
2647 
2648 
2649 class TransInfo():
2650  '''
2651  This class provides information about, and access, to the available
2652  transformations in this implementation of PSyclone. New transformations
2653  will be picked up automatically as long as they subclass the abstract
2654  Transformation class.
2655 
2656  For example:
2657 
2658  >>> from psyclone.psyGen import TransInfo
2659  >>> t = TransInfo()
2660  >>> print(t.list)
2661  There is 1 transformation available:
2662  1: SwapTrans, A test transformation
2663  >>> # accessing a transformation by index
2664  >>> trans = t.get_trans_num(1)
2665  >>> # accessing a transformation by name
2666  >>> trans = t.get_trans_name("SwapTrans")
2667 
2668  '''
2669 
2670  def __init__(self, module=None, base_class=None):
2671  ''' if module and/or baseclass are provided then use these else use
2672  the default module "Transformations" and the default base_class
2673  "Transformation"'''
2674 
2675  if False:
2676  self._0_to_n_0_to_n = DummyTransformation() # only here for pyreverse!
2677 
2678  # TODO #620: This need to be improved to support the new
2679  # layout, where transformations are in different directories and files.
2680  # Leaving local imports so they will be removed once TransInfo is
2681  # replaced.
2682  # pylint: disable=import-outside-toplevel
2683  from psyclone import transformations
2684  if module is None:
2685  # default to the transformation module
2686  module = transformations
2687  if base_class is None:
2688  base_class = Transformation
2689  # find our transformations
2690  self._classes_classes = self._find_subclasses_find_subclasses(module, base_class)
2691 
2692  # create our transformations
2693  self._objects_objects = []
2694  self._obj_map_obj_map = {}
2695  for my_class in self._classes_classes:
2696  my_object = my_class()
2697  self._objects_objects.append(my_object)
2698  self._obj_map_obj_map[my_object.name] = my_object
2699  # TODO #620:
2700  # Transformations that are in psyir and other subdirectories
2701  # are not found by TransInfo, so we add some that are used in
2702  # tests and examples explicitly. I'm leaving this import here
2703  # so it is obvious it can be removed.
2704  from psyclone.psyir.transformations import LoopFuseTrans
2705  my_object = LoopFuseTrans()
2706  # Only add the loop-fuse statement if base_class and module
2707  # match for the loop fusion transformation.
2708  if isinstance(my_object, base_class) and module == transformations:
2709  self._objects_objects.append(LoopFuseTrans())
2710  self._obj_map_obj_map["LoopFuseTrans"] = self._objects_objects[-1]
2711 
2712  @property
2713  def list(self):
2714  ''' return a string with a human readable list of the available
2715  transformations '''
2716  import os
2717  if len(self._objects_objects) == 1:
2718  result = "There is 1 transformation available:"
2719  else:
2720  result = (f"There are {len(self._objects)} transformations "
2721  f"available:")
2722  result += os.linesep
2723  for idx, my_object in enumerate(self._objects_objects):
2724  result += " " + str(idx+1) + ": " + my_object.name + ": " + \
2725  str(my_object) + os.linesep
2726  return result
2727 
2728  @property
2729  def num_trans(self):
2730  ''' return the number of transformations available '''
2731  return len(self._objects_objects)
2732 
2733  def get_trans_num(self, number):
2734  ''' return the transformation with this number (use list() first to
2735  see available transformations) '''
2736  if number < 1 or number > len(self._objects_objects):
2737  raise GenerationError("Invalid transformation number supplied")
2738  return self._objects_objects[number-1]
2739 
2740  def get_trans_name(self, name):
2741  ''' return the transformation with this name (use list() first to see
2742  available transformations) '''
2743  try:
2744  return self._obj_map_obj_map[name]
2745  except KeyError:
2746  raise GenerationError(f"Invalid transformation name: got {name} "
2747  f"but expected one of "
2748  f"{self._obj_map.keys()}")
2749 
2750  def _find_subclasses(self, module, base_class):
2751  ''' return a list of classes defined within the specified module that
2752  are a subclass of the specified baseclass. '''
2753  import inspect
2754  return [cls for name, cls in inspect.getmembers(module)
2755  if inspect.isclass(cls) and not inspect.isabstract(cls) and
2756  issubclass(cls, base_class) and cls is not base_class]
2757 
2758 
2759 class Transformation(metaclass=abc.ABCMeta):
2760  '''Abstract baseclass for a transformation. Uses the abc module so it
2761  can not be instantiated.
2762 
2763  '''
2764  @property
2765  def name(self):
2766  '''
2767  :returns: the transformation's class name.
2768  :rtype: str
2769 
2770  '''
2771  return type(self).__name__
2772 
2773  @abc.abstractmethod
2774  def apply(self, node, options=None):
2775  '''Abstract method that applies the transformation. This function
2776  must be implemented by each transform. As a minimum each apply
2777  function must take a node to which the transform is applied, and
2778  a dictionary of additional options, which will also be passed on
2779  to the validate functions. This dictionary is used to provide
2780  optional parameters, and also to modify the behaviour of
2781  validation of transformations: for example, if the user knows that
2782  a transformation can correctly be applied in a specific case, but
2783  the more generic code validation would not allow this. Validation
2784  functions should check for a key in the options dictionary to
2785  disable certain tests. Those keys will be documented in each
2786  apply() and validate() function.
2787 
2788  Note that some apply() functions might take a slightly different
2789  set of parameters.
2790 
2791  :param node: The node (or list of nodes) for the transformation \
2792  - specific to the actual transform used.
2793  :type node: depends on actual transformation
2794  :param options: a dictionary with options for transformations.
2795  :type options: Optional[Dict[str, Any]]
2796 
2797  '''
2798 
2799  def validate(self, node, options=None):
2800  '''Method that validates that the input data is correct.
2801  It will raise exceptions if the input data is incorrect. This
2802  function needs to be implemented by each transformation.
2803 
2804  The validate function can be called by the user independent of
2805  the apply() function, but it will automatically be executed as
2806  part of an apply() call.
2807 
2808  As minimum each validate function must take a node to which the
2809  transform is applied and a dictionary of additional options.
2810  This dictionary is used to provide optional parameters and also
2811  to modify the behaviour of validation: for example, if the user
2812  knows that a transformation can correctly be applied in a specific
2813  case but the more generic code validation would not allow this.
2814  Validation functions should check for particular keys in the options
2815  dict in order to disable certain tests. Those keys will be documented
2816  in each apply() and validate() function as 'options["option-name"]'.
2817 
2818  Note that some validate functions might take a slightly different
2819  set of parameters.
2820 
2821  :param node: The node (or list of nodes) for the transformation \
2822  - specific to the actual transform used.
2823  :type node: depends on actual transformation
2824  :param options: a dictionary with options for transformations.
2825  :type options: Optional[Dict[str, Any]]
2826  '''
2827  # pylint: disable=unused-argument
2828 
2829 
2830 class DummyTransformation(Transformation):
2831  '''Dummy transformation use elsewhere to keep pyreverse happy.'''
2832 
2833  @property
2834  def name(self):
2835  return
2836 
2837  def apply(self, node, options=None):
2838  pass
2839 
2840 
2841 # For Sphinx AutoAPI documentation generation
2842 __all__ = ['PSyFactory', 'PSy', 'Invokes', 'Invoke', 'InvokeSchedule',
2843  'GlobalSum', 'HaloExchange', 'Kern', 'CodedKern', 'InlinedKern',
2844  'BuiltIn', 'Arguments', 'DataAccess', 'Argument', 'KernelArgument',
2845  'TransInfo', 'Transformation', 'DummyTransformation']
def backward_write_dependencies(self, ignore_halos=False)
Definition: psyGen.py:2396
def access(self, value)
Definition: psyGen.py:2282
def _depends_on(self, argument)
Definition: psyGen.py:2555
def _find_write_arguments(self, nodes, ignore_halos=False)
Definition: psyGen.py:2502
def _find_read_arguments(self, nodes)
Definition: psyGen.py:2463
def forward_read_dependencies(self)
Definition: psyGen.py:2429
def intrinsic_type(self)
Definition: psyGen.py:2312
def infer_datatype(self)
Definition: psyGen.py:2247
def psyir_expression(self)
Definition: psyGen.py:2240
def _find_argument(self, nodes)
Definition: psyGen.py:2444
def forward_dependence(self)
Definition: psyGen.py:2416
def backward_dependence(self)
Definition: psyGen.py:2364
def argument_type(self)
Definition: psyGen.py:2298
def forward_write_dependencies(self, ignore_halos=False)
Definition: psyGen.py:2377
def iteration_space_arg(self)
Definition: psyGen.py:1940
def append(self, name, argument_type)
Definition: psyGen.py:1978
def psyir_expressions(self)
Definition: psyGen.py:1920
def local_vars(self)
Definition: psyGen.py:1896
def load(self, call, arguments, parent=None)
Definition: psyGen.py:1891
def module_inline(self, value)
Definition: psyGen.py:1478
def set_opencl_options(self, options)
Definition: psyGen.py:1417
def lower_to_language_level(self)
Definition: psyGen.py:1516
def node_str(self, colour=True)
Definition: psyGen.py:1503
def _rename_psyir(self, suffix)
Definition: psyGen.py:1735
def get_kernel_schedule(self)
Definition: psyGen.py:1393
def _new_name(original, tag, suffix)
Definition: psyGen.py:1607
def incremented_arg(self)
Definition: psyGen.py:1567
def rename_and_write(self)
Definition: psyGen.py:1623
def opencl_options(self)
Definition: psyGen.py:1410
def modified(self, value)
Definition: psyGen.py:1800
def overlaps(self, arg)
Definition: psyGen.py:2019
def __init__(self, arg)
Definition: psyGen.py:1996
def update_coverage(self, arg)
Definition: psyGen.py:2072
def apply(self, node, options=None)
Definition: psyGen.py:2837
def node_str(self, colour=True)
Definition: psyGen.py:844
def check_vector_halos_differ(self, node)
Definition: psyGen.py:941
def node_str(self, colour=True)
Definition: psyGen.py:1000
def node_str(self, colour=True)
Definition: psyGen.py:1852
def node_str(self, colour=True)
Definition: psyGen.py:751
def gen_code(self, parent)
Definition: psyGen.py:770
def unique_declns_by_intent(self, argument_types, intrinsic_type=None)
Definition: psyGen.py:588
def unique_declarations(self, argument_types, access=None, intrinsic_type=None)
Definition: psyGen.py:516
def __init__(self, alg_invocation, idx, schedule_class, invokes, reserved_names=None)
Definition: psyGen.py:403
def gen_code(self, parent)
Definition: psyGen.py:669
def schedule(self, obj)
Definition: psyGen.py:512
def first_access(self, arg_name)
Definition: psyGen.py:577
def invokes(self)
Definition: psyGen.py:480
def schedule(self)
Definition: psyGen.py:508
def __init__(self, alg_calls, invoke_cls, psy)
Definition: psyGen.py:311
def get(self, invoke_name)
Definition: psyGen.py:336
def gen_code(self, parent)
Definition: psyGen.py:364
def is_reduction(self)
Definition: psyGen.py:1111
def name(self, value)
Definition: psyGen.py:1321
def arg_descriptors(self, obj)
Definition: psyGen.py:1305
def reduction_arg(self)
Definition: psyGen.py:1120
def name(self)
Definition: psyGen.py:1313
def arguments(self)
Definition: psyGen.py:1309
def args(self)
Definition: psyGen.py:1080
def reference_accesses(self, var_accesses)
Definition: psyGen.py:1097
def node_str(self, colour=True)
Definition: psyGen.py:1085
def zero_reduction_variable(self, parent, position=None)
Definition: psyGen.py:1158
def reduction_sum_loop(self, parent)
Definition: psyGen.py:1224
def arg_descriptors(self)
Definition: psyGen.py:1301
def _reduction_reference(self)
Definition: psyGen.py:1265
def is_coloured(self)
Definition: psyGen.py:1329
def reprod_reduction(self)
Definition: psyGen.py:1130
def local_reduction_name(self)
Definition: psyGen.py:1145
def create(self, invoke_info)
Definition: psyGen.py:199
def name(self)
Definition: psyGen.py:282
def invokes(self)
Definition: psyGen.py:275
def gen(self)
Definition: psyGen.py:290
def container(self)
Definition: psyGen.py:264
def get_trans_num(self, number)
Definition: psyGen.py:2733
def get_trans_name(self, name)
Definition: psyGen.py:2740
def __init__(self, module=None, base_class=None)
Definition: psyGen.py:2670
def _find_subclasses(self, module, base_class)
Definition: psyGen.py:2750
def apply(self, node, options=None)
Definition: psyGen.py:2774
def validate(self, node, options=None)
Definition: psyGen.py:2799