Reference Guide  2.5.0
kernel.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: L. Mitchell Imperial College
35 # R. W. Ford, A. R. Porter and N. Nobre, STFC Daresbury Lab
36 # Modified: C.M. Maynard, Met Office / University of Reading,
37 # I. Kavcic and L. Turner, Met Office
38 # J. Henrichs, Bureau of Meteorology
39 
40 '''Module that uses the Fortran parser fparser1 to parse
41 PSyclone-conformant kernel code.
42 
43 '''
44 
45 import os
46 import sys
47 
48 from pyparsing import ParseException
49 
50 import fparser
51 from fparser.two.parser import ParserFactory
52 from fparser.two import Fortran2003
53 from fparser.two.utils import walk
54 
55 from fparser import one as fparser1
56 from fparser import api as fpapi
57 from fparser.one import parsefortran
58 
59 import psyclone.expression as expr
60 from psyclone.errors import InternalError
61 from psyclone.configuration import Config
62 from psyclone.parse.utils import check_api, check_line_length, ParseError
63 
64 
65 def get_kernel_filepath(module_name, kernel_paths, alg_filename):
66  '''Search for a kernel module file containing a module with
67  'module_name'. The assumed convention is that the name of the
68  kernel file is the name of the module with .f90 or .F90 appended.
69 
70  Look in the directories and all subdirectories associated with the
71  supplied kernel paths or in the same directory as the algorithm
72  file if not found within the kernel paths.
73 
74  Return the filepath if the file is found.
75 
76  :param str module_name: the name of the module to search for. The \
77  assumption is that the file containing the module will have the \
78  same name as the module name with .f90 or .F90 appended.
79  :param kernel_paths: directories in which to search for the module \
80  file. If nothing is supplied then look in the same directory \
81  as the algorithm file. Directories below the specified \
82  directories are recursively searched.
83  :type list_kernels: list of str
84  :param str alg_filename: the name of the algorithm file. This is \
85  used to determine its directory location if required.
86 
87  :returns: a filepath to the file containing the specified module \
88  name.
89  :rtype: str
90 
91  :raises ParseError: if the supplied kernel directory does not \
92  exist.
93  :raises ParseError: if the file can not be found.
94  :raises ParseError: if more than one file with the specified name \
95  is found.
96 
97  '''
98  # Only consider files with the suffixes .f90 and .F90 when
99  # searching for the kernel source (we perform a case insensitive
100  # match).
101  search_string = f"{module_name}.F90"
102  matches = []
103 
104  # If a search path has been specified then look there. Otherwise
105  # look in the directory containing the algorithm definition file.
106  for kernel_path in kernel_paths:
107  # Look for the file in the supplied directory and recursively
108  # in any subdirectories.
109  cdir = os.path.abspath(kernel_path)
110 
111  if not os.access(cdir, os.R_OK):
112  raise ParseError(
113  f"kernel.py:get_kernel_filepath: Supplied kernel search path "
114  f"does not exist or cannot be read: {cdir}")
115 
116  # Recursively search down through the directory tree starting
117  # at the specified path.
118  for root, _, filenames in os.walk(cdir):
119  for filename in filenames:
120  # perform a case insensitive match
121  if filename.lower() == search_string.lower():
122  matches.append(os.path.join(root, filename))
123  if not kernel_paths:
124  # Look *only* in the directory that contained the algorithm
125  # file.
126  cdir = os.path.abspath(os.path.dirname(alg_filename))
127  filenames = os.listdir(cdir)
128  for filename in filenames:
129  # perform a case insensitive match
130  if filename.lower() == search_string.lower():
131  matches.append(os.path.join(cdir, filename))
132 
133  if not matches:
134  # There were no matches.
135  raise ParseError(
136  f"Kernel file '{module_name}.[fF]90' not found in {cdir}")
137  if len(matches) > 1:
138  # There was more than one match
139  raise ParseError(
140  f"kernel.py:get_kernel_filepath: More than one match for kernel "
141  f"file '{module_name}.[fF]90' found!")
142  # There is a single match
143  return matches[0]
144 
145 
146 def get_kernel_parse_tree(filepath):
147  '''Parse the file in filepath with fparser1 and return a parse tree.
148 
149  :param str filepath: path to a file (hopefully) containing \
150  PSyclone kernel code.
151 
152  :returns: Parse tree of the kernel code contained in the specified \
153  file.
154  :rtype: :py:class:`fparser.one.block_statements.BeginSource`
155 
156  :raises ParseError: if fparser fails to parse the file
157 
158  '''
159  parsefortran.FortranParser.cache.clear()
160 
161  # If logging is disable during a sphinx doctest run, doctest will just
162  # stop working. So only disable logging if we are not running doctest.
163  if 'sphinx.ext.doctest' not in sys.modules:
164  fparser.logging.disable(fparser.logging.CRITICAL)
165 
166  try:
167  parse_tree = fpapi.parse(filepath)
168  # parse_tree includes an extra comment line which contains
169  # file details. This line can be long which can cause line
170  # length issues. Therefore set the information (name) to be
171  # empty.
172  parse_tree.name = ""
173  except Exception as err:
174  raise ParseError(
175  f"Failed to parse kernel code '{filepath}'. Is the Fortran "
176  f"correct?") from err
177  return parse_tree
178 
179 
180 def get_kernel_ast(module_name, alg_filename, kernel_paths, line_length):
181  '''Search for the kernel source code containing a module with the name
182  'module_name' looking in the directory and subdirectories
183  associated with the supplied 'kernel_paths' or in the same
184  directory as the 'algorithm_filename' if the kernel path is
185  empty. If the file is found then check it conforms to the
186  'line_length' restriction if this is set and then parse this file
187  and return the parsed file.
188 
189  :param str module_name: the name of the module to search for.
190  :param str alg_filename: the name of the algorithm file.
191  :param kernel_paths: directory in which to search for the module \
192  file.
193  :type kernel_paths: list of str
194  :param bool line_length: whether to check that the kernel code \
195  conforms to the 132 character line length limit (True) or not \
196  (False).
197 
198  :returns: Parse tree of the kernel module with the name 'module_name'.
199  :rtype: :py:class:`fparser.one.block_statements.BeginSource`
200 
201  '''
202  filepath = get_kernel_filepath(module_name, kernel_paths, alg_filename)
203  if line_length:
204  check_line_length(filepath)
205  parse_tree = get_kernel_parse_tree(filepath)
206  return parse_tree
207 
208 
209 # pylint: disable=too-few-public-methods
211  '''Factory to create the required API-specific information about
212  coded-kernel metadata and a reference to its code.
213 
214  :param str api: The API for which this factory is to create Kernel \
215  information. If it is not supplied then the default API, as \
216  specified in the PSyclone config file, is used.
217 
218  '''
219  def __init__(self, api=""):
220  if not api:
221  _config = Config.get()
222  self._type_type = _config.default_api
223  else:
224  check_api(api)
225  self._type_type = api
226 
227  def create(self, parse_tree, name=None):
228  '''Create API-specific information about the kernel metadata and a
229  reference to its code. The API is set when the factory is
230  created.
231 
232  :param parse_tree: The fparser1 parse tree for the Kernel code.
233  :type parse_tree: :py:class:`fparser.one.block_statements.BeginSource`
234 
235  :param name: the name of the Kernel. Defaults to None if \
236  one is not provided.
237  :type name: str or NoneType
238 
239  :raises ParseError: if the supplied API is not supported.
240 
241  '''
242  # Avoid circular import
243  # pylint: disable=import-outside-toplevel
244  if self._type_type == "dynamo0.3":
245  from psyclone.domain.lfric import LFRicKernMetadata
246  return LFRicKernMetadata(parse_tree, name=name)
247  if self._type_type == "gocean1.0":
248  from psyclone.gocean1p0 import GOKernelType1p0
249  return GOKernelType1p0(parse_tree, name=name)
250  raise ParseError(
251  f"KernelTypeFactory:create: Unsupported kernel type '{self._type}'"
252  f" found.")
253 
254 
256  '''Create API-specific information about the builtin metadata. The API
257  is set when the factory is created. Subclasses KernelTypeFactory
258  and makes use of its init method.
259 
260  '''
261  # pylint: disable=arguments-differ
262  def create(self, builtin_names, builtin_defs_file, name=None):
263  '''Create API-specific information about the builtin metadata. This
264  method finds and parses the metadata then makes use of the
265  KernelTypeFactory parent class to return the api-specific
266  information about the builtin.
267 
268  :param builtin_names: a list of valid builtin names
269  :type builtin_names: list of str
270  :param str builtin_defs_file: the file containing builtin \
271  metadata
272  :param name: the name of the builtin. Defaults to None if \
273  one is not provided.
274  :type name: str or NoneType
275 
276  :raises ParseError: if the supplied name is not one of the \
277  builtin names
278  :raises ParseError: if the supplied name is recognised as a \
279  builtin but the associated file containing the required \
280  metadata can not be found.
281  :raises ParseError: if the metadata for the supplied builtin \
282  can not be parsed.
283 
284  '''
285  if name not in builtin_names:
286  raise ParseError(
287  f"BuiltInKernelTypeFactory:create unrecognised built-in name. "
288  f"Got '{name}' but expected one of {builtin_names}")
289  # The meta-data for these lives in a Fortran module file
290  # passed in to this method.
291  fname = os.path.join(
292  os.path.dirname(os.path.abspath(__file__)),
293  builtin_defs_file)
294  if not os.path.isfile(fname):
295  raise ParseError(
296  f"BuiltInKernelTypeFactory:create Kernel '{name}' is a "
297  f"recognised Built-in but cannot find file '{fname}' "
298  f"containing the meta-data describing the Built-in operations "
299  f"for API '{self._type}'")
300  # Attempt to parse the meta-data
301  try:
302  parsefortran.FortranParser.cache.clear()
303  fparser.logging.disable(fparser.logging.CRITICAL)
304  parse_tree = fpapi.parse(fname)
305  except Exception as err:
306  raise ParseError(
307  f"BuiltInKernelTypeFactory:create: Failed to parse the meta-"
308  f"data for PSyclone built-ins in file '{fname}'.") from err
309 
310  # Now we have the parse tree, call our parent class to create \
311  # the object
312  return KernelTypeFactory.create(self, parse_tree, name)
313 # pylint: enable=too-few-public-methods
314 
315 
316 def get_mesh(metadata, valid_mesh_types):
317  '''
318  Returns the mesh-type described by the supplied meta-data
319 
320  :param metadata: node in parser ast
321  :type metadata: py:class:`psyclone.expression.NamedArg`
322  :param valid_mesh_types: List of valid mesh types
323  :type valid_mesh_types: list of strings
324 
325  :return: the name of the mesh
326  :rtype: string
327 
328  :raises ParseError: if the supplied meta-data is not a recognised \
329  mesh identifier.
330  :raises ParseError: if the mesh type is unsupported.
331 
332  '''
333  if not isinstance(metadata, expr.NamedArg) or \
334  metadata.name.lower() != "mesh_arg":
335  raise ParseError(
336  f"{metadata} is not a valid mesh identifier (expected "
337  f"mesh_arg=MESH_TYPE where MESH_TYPE is one of "
338  f"{valid_mesh_types}))")
339  mesh = metadata.value.lower()
340  if mesh not in valid_mesh_types:
341  raise ParseError(f"mesh_arg must be one of {valid_mesh_types} but got "
342  f"{mesh}")
343  return mesh
344 
345 
346 def get_stencil(metadata, valid_types):
347  '''Returns stencil_type and stencil_extent as a dictionary
348  object from stencil metadata if the metadata conforms to the
349  stencil(type[,extent]) format
350 
351  :param metadata: Component of kernel meta-data stored as a node in \
352  the fparser1 AST
353  :type metadata: :py:class:`psyclone.expression.FunctionVar`
354  :param list valid_types: List of valid stencil types (strings)
355 
356  :return: The stencil type and extent described in the meta-data
357  :rtype: dict with keys 'type' (str) and 'extent' (int)
358 
359  :raises ParseError: if the supplied meta-data is not a recognised \
360  stencil specification
361 
362  '''
363 
364  if not isinstance(metadata, expr.FunctionVar):
365  raise ParseError(
366  f"Expecting format stencil(<type>[,<extent>]) but found the "
367  f"literal {metadata}")
368  if metadata.name.lower() != "stencil" or not metadata.args:
369  raise ParseError(
370  f"Expecting format stencil(<type>[,<extent>]) but found {metadata}"
371  )
372  if len(metadata.args) > 2:
373  raise ParseError(
374  f"Expecting format stencil(<type>[,<extent>]) so there must "
375  f"be at most two arguments inside the brackets {metadata}")
376  if not isinstance(metadata.args[0], expr.FunctionVar):
377  if isinstance(metadata.args[0], str):
378  raise ParseError(
379  f"Expecting format stencil(<type>[,<extent>]). However, "
380  f"the specified <type> '{metadata.args[0]}' is a literal and "
381  f"therefore is not one of the valid types '{valid_types}'")
382  raise ParseError(
383  f"Internal error, expecting either FunctionVar or str from the "
384  f"expression analyser but found {type(metadata.args[0])}")
385  if metadata.args[0].args:
386  raise ParseError(
387  "Expected format stencil(<type>[,<extent>]). However, the "
388  "specified <type> '{0}' includes brackets")
389  stencil_type = metadata.args[0].name
390  if stencil_type not in valid_types:
391  raise ParseError(
392  f"Expected format stencil(<type>[,<extent>]). However, the "
393  f"specified <type> '{stencil_type}' is not one of the valid types "
394  f"'{valid_types}'")
395 
396  stencil_extent = None
397  if len(metadata.args) == 2:
398  if not isinstance(metadata.args[1], str):
399  raise ParseError(
400  f"Expected format stencil(<type>[,<extent>]). However, the "
401  f"specified <extent> '{metadata.args[1]}' is not an integer")
402  stencil_extent = int(metadata.args[1])
403  if stencil_extent < 1:
404  raise ParseError(
405  f"Expected format stencil(<type>[,<extent>]). However, the "
406  f"specified <extent> '{stencil_extent}' is less than 1")
407  raise NotImplementedError(
408  "Kernels with fixed stencil extents are not currently "
409  "supported")
410  return {"type": stencil_type, "extent": stencil_extent}
411 
412 
413 class Descriptor():
414  '''
415  A description of how a kernel argument is accessed, constructed from
416  the kernel metadata.
417 
418  :param str access: whether argument is read/write etc.
419  :param str space: which function space/grid-point type argument is on.
420  :param int metadata_index: position of this argument in the list of \
421  arguments specified in the metadata.
422  :param dict stencil: type of stencil access for this argument. \
423  Defaults to None if the argument is not supplied.
424  :param str mesh: which mesh this argument is on. Defaults to None \
425  if the argument is not supplied.
426  :param str argument_type: the type of this argument. Defaults to \
427  None if the argument is not supplied.
428 
429  :raises InternalError: if the metadata_index argument is not an int or is \
430  less than zero.
431 
432  '''
433  # pylint: disable=too-many-arguments
434  def __init__(self, access, space, metadata_index, stencil=None, mesh=None,
435  argument_type=None):
436  self._access_access = access
437  self._space_space = space
438  if not isinstance(metadata_index, int) or metadata_index < 0:
439  raise InternalError(
440  f"The metadata index must be an integer and greater than or "
441  f"equal to zero but got: {metadata_index}")
442  self._metadata_index_metadata_index = metadata_index
443  self._stencil_stencil = stencil
444  self._mesh_mesh = mesh
445  self._argument_type_argument_type = argument_type
446 
447  @property
448  def access(self):
449  '''
450  :returns: whether argument is read/write etc.
451  :rtype: str
452 
453  '''
454  return self._access_access
455 
456  @property
457  def function_space(self):
458  '''
459  :returns: which function space/grid-point type argument is on.
460  :rtype: str
461 
462  '''
463  return self._space_space
464 
465  @property
466  def metadata_index(self):
467  '''
468  :returns: the position of the corresponding argument descriptor in \
469  the kernel metadata.
470  :rtype: int
471  '''
472  return self._metadata_index_metadata_index
473 
474  @property
475  def stencil(self):
476  '''
477  :returns: type of stencil access for this argument.
478  :rtype: dict or NoneType
479 
480  '''
481  return self._stencil_stencil
482 
483  @property
484  def mesh(self):
485  '''
486  :returns: the mesh the argument is on.
487  :rtype: str or NoneType
488 
489  '''
490  return self._mesh_mesh
491 
492  @property
493  def argument_type(self):
494  '''
495  :returns: the type of the argument depending on the specific \
496  API (e.g. scalar, field, grid property, operator).
497  :rtype: str or NoneType
498 
499  '''
500  return self._argument_type_argument_type
501 
502  def __repr__(self):
503  return (f"Descriptor({self.access}, {self.function_space}, "
504  f"{self.metadata_index})")
505 
506 
508  '''
509  Captures the parse tree and name of a kernel subroutine.
510 
511  :param ktype_ast: the fparser1 parse tree for the Kernel meta-data.
512  :type ktype_ast: :py:class:`fparser.one.block_statements.Type`
513  :param str ktype_name: name of the Fortran type holding the Kernel \
514  meta-data.
515  :param modast: the fparser1 parse tree for the module containing the \
516  Kernel routine.
517  :type modast: :py:class:`fparser.one.block_statements.BeginSource`
518 
519  '''
520  def __init__(self, ktype_ast, ktype_name, modast):
521  self._ast, self._name_name = KernelProcedure.get_procedure(
522  ktype_ast, ktype_name, modast)
523 
524  # pylint: disable=too-many-branches
525  @staticmethod
526  def get_procedure(ast, name, modast):
527  '''
528  Get the name of the subroutine associated with the Kernel. This is
529  a type-bound procedure in the meta-data which may take one of three
530  forms:
531  PROCEDURE, nopass :: code => <proc_name>
532  or
533  PROCEDURE, nopass :: <proc_name>
534  or if there is no type-bound procedure, an interface may be used:
535  INTERFACE <proc_name>
536 
537  :param ast: the fparser1 parse tree for the Kernel meta-data.
538  :type ast: :py:class:`fparser.one.block_statements.Type`
539  :param str name: the name of the Fortran type holding the Kernel \
540  meta-data.
541  :param modast: the fparser1 parse tree for the module containing the \
542  Kernel routine.
543  :type modast: :py:class:`fparser.one.block_statements.BeginSource`
544 
545  :returns: 2-tuple of the fparser1 parse tree of the Subroutine \
546  statement and the name of that Subroutine.
547  :rtype: (:py:class:`fparser1.block_statements.Subroutine`, str)
548 
549  :raises ParseError: if the supplied Kernel meta-data does not \
550  have a type-bound procedure or interface.
551  :raises ParseError: if no implementation is found for the \
552  type-bound procedure or interface module \
553  procedures.
554  :raises ParseError: if the type-bound procedure specifies a binding \
555  name but the generic name is not "code".
556  :raises InternalError: if we get an empty string for the name of the \
557  type-bound procedure.
558  '''
559  bname = None
560  # Search the the meta-data for a SpecificBinding
561  for statement in ast.content:
562  if isinstance(statement, fparser1.statements.SpecificBinding):
563  # We support:
564  # PROCEDURE, nopass :: code => <proc_name> or
565  # PROCEDURE, nopass :: <proc_name>
566  if statement.bname:
567  if statement.name.lower() != "code":
568  raise ParseError(
569  f"Kernel type {name} binds to a specific procedure"
570  f" but does not use 'code' as the generic name.")
571  bname = statement.bname.lower()
572  else:
573  bname = statement.name.lower()
574  break
575  if bname is None:
576  # If no type-bound procedure found, search for an explicit
577  # interface that has module procedures.
578  bname, subnames = get_kernel_interface(name, modast)
579  if bname is None:
580  # no interface found either
581  raise ParseError(
582  f"Kernel type {name} does not bind a specific procedure "
583  f"or provide an explicit interface")
584  elif bname == '':
585  raise InternalError(
586  f"Empty Kernel name returned for Kernel type {name}.")
587  else:
588  # add the name of the tbp to the list of strings to search for.
589  subnames = [bname]
590  # walk the AST to check the subroutine names exist.
591  procedure_count = 0
592  for subname in subnames:
593  for statement, _ in fpapi.walk(modast):
594  if isinstance(statement,
595  fparser1.block_statements.Subroutine) \
596  and statement.name.lower() \
597  == subname:
598  procedure_count = procedure_count + 1
599  if procedure_count == 1:
600  # set code to statement if there is one procedure.
601  code = statement
602  else:
603  code = None # set to None if there is more than one.
604  break
605  else:
606  raise ParseError(
607  f"kernel.py:KernelProcedure:get_procedure: Kernel "
608  f"subroutine '{subname}' not found.")
609  return code, bname
610 
611  @property
612  def name(self):
613  '''
614  :returns: the name of the kernel subroutine
615  :rtype: str
616 
617  '''
618  return self._name_name
619 
620  @property
621  def ast(self):
622  '''
623  :returns: the parse tree of the kernel subroutine
624  :rtype: :py:class:`fparser.one.block_statements.Subroutine`
625 
626  '''
627  return self._ast
628 
629  def __repr__(self):
630  return f"KernelProcedure({self.name})"
631 
632  def __str__(self):
633  return str(self._ast)
634 
635 
636 def get_kernel_metadata(name, ast):
637  '''Takes the kernel module parse tree and returns the metadata part
638  of the parse tree (a Fortran type) with the name 'name'.
639 
640  :param str name: the metadata name (of a Fortran type). Also \
641  the name referencing the kernel in the algorithm layer. The name \
642  provided and the name of the kernel in the parse tree are case \
643  insensitive in this function.
644  :param ast: parse tree of the kernel module code
645  :type ast: :py:class:`fparser.one.block_statements.BeginSource`
646 
647  :returns: Parse tree of the metadata (a Fortran type with name \
648  'name')
649  :rtype: :py:class:`fparser.one.block_statements.Type`
650 
651  :raises ParseError: if the metadata type name is not found in \
652  the kernel code parse tree
653 
654  '''
655  ktype = None
656  for statement, _ in fpapi.walk(ast):
657  if isinstance(statement, fparser1.block_statements.Type) \
658  and statement.name.lower() == name.lower():
659  ktype = statement
660  break
661  if ktype is None:
662  raise ParseError(f"Kernel type {name} does not exist")
663  return ktype
664 
665 
666 def get_kernel_interface(name, ast):
667  '''Takes the kernel module parse tree and returns the interface part
668  of the parse tree.
669 
670  :param str name: The kernel name
671  :param ast: parse tree of the kernel module code
672  :type ast: :py:class:`fparser.one.block_statements.BeginSource`
673 
674  :returns: Name of the interface block and the names of the module \
675  procedures (lower case). Or None, None if there is no \
676  interface or the interface has no nodule procedures.
677  :rtype: : `str`, list of str`.
678 
679  :raises ParseError: if more than one interface is found.
680  '''
681 
682  iname = None
683  sname = None
684  count = 0
685  for statement, _ in fpapi.walk(ast):
686  if isinstance(statement, fparser1.block_statements.Interface):
687  # count the interfaces, then can be only one!
688  count = count + 1
689  if count >= 2:
690  raise ParseError(f"Module containing kernel {name} has more "
691  f"than one interface, this is forbidden in "
692  f"the LFRic API.")
693  # Check the interfaces assigns one or more module procedures.
694  if statement.a.module_procedures:
695  iname = statement.name.lower()
696  # If implicit interface (no name) set to none as there is no
697  # procedure name for PSyclone to use.
698  if iname == '':
699  iname = None
700  else:
701  sname = [str(sname).lower() for sname
702  in statement.a.module_procedures]
703  return iname, sname
704 
705 
706 def getkerneldescriptors(name, ast, var_name='meta_args', var_type=None):
707  '''Get the required argument metadata information for a kernel.
708 
709  :param str name: the name of the kernel (for error messages).
710  :param ast: metadata describing kernel arguments.
711  :type ast: :py:class:`fparser.one.block_statements.Type`
712  :param str var_name: the name of the variable storing the \
713  argument metadata. this argument is optional and defaults to \
714  'meta_args'.
715  :param str var_type: the type of the structure constructor used to \
716  define the meta-data or None.
717 
718  :returns: argument metadata parsed using the expression parser \
719  (as fparser1 will not parse expressions and arguments).
720  :rtype: :py:class:`psyclone.expression.LiteralArray`
721 
722  :raises ParseError: if 'var_name' is not found in the metadata.
723  :raises ParseError: if 'var_name' is not an array.
724  :raises ParseError: if 'var_name' is not a 1D array.
725  :raises ParseError: if the structure constructor uses '[...]' \
726  as only '(/.../)' is supported.
727  :raises ParseError: if the argument metadata is invalid and cannot \
728  be parsed.
729  :raises ParseError: if the dimensions specified do not tally with \
730  the number of metadata arguments.
731  :raises ParseError: if var_type is specified and a structure \
732  constructor for a different type is found.
733  '''
734  descs = ast.get_variable(var_name)
735  if "INTEGER" in str(descs):
736  # INTEGER in above 'if' test is an fparser1 hack as get_variable()
737  # returns an integer if the variable is not found.
738  raise ParseError(
739  f"No variable named '{var_name}' found in the metadata for kernel "
740  f"'{name}': '{str(ast).strip()}'.")
741  try:
742  nargs = int(descs.shape[0])
743  except AttributeError as err:
744  raise ParseError(
745  f"In kernel metadata '{name}': '{var_name}' variable must be an "
746  f"array.") from err
747  if len(descs.shape) != 1:
748  raise ParseError(
749  f"In kernel metadata '{name}': '{var_name}' variable must be a 1 "
750  f"dimensional array.")
751  if descs.init.find("[") != -1 and descs.init.find("]") != -1:
752  # there is a bug in fparser1
753  raise ParseError(
754  f"Parser does not currently support '[...]' initialisation for "
755  f"'{var_name}', please use '(/.../)' instead.")
756  try:
757  inits = expr.FORT_EXPRESSION.parseString(descs.init)[0]
758  except ParseException as err:
759  raise ParseError(f"Kernel metadata has an invalid format "
760  f"{descs.init}.") from err
761  nargs = int(descs.shape[0])
762  if len(inits) != nargs:
763  raise ParseError(
764  f"In the '{var_name}' metadata, the number of items in the array "
765  f"constructor ({len(inits)}) does not match the extent of the "
766  f"array ({nargs}).")
767  if var_type:
768  # Check that each element in the list is of the correct type
769  if not all(init.name == var_type for init in inits):
770  raise ParseError(
771  f"The '{var_name}' metadata must consist of an array of "
772  f"structure constructors, all of type '{var_type}' but found: "
773  f"{[str(init.name) for init in inits]}.")
774  return inits
775 
776 
777 class KernelType():
778  '''Base class for describing Kernel Metadata.
779 
780  This contains the name of the elemental procedure and metadata associated
781  with how that procedure is mapped over mesh entities.
782 
783  :param ast: fparser1 AST for the parsed kernel meta-data.
784  :type ast: :py:class:`fparser.one.block_statements.BeginSource`
785  :param str name: name of the Fortran derived type describing the kernel.
786 
787  :raises ParseError: if the supplied AST does not contain a Fortran \
788  module.
789  :raises ParseError: if the module name is too short to contain \
790  '_mod' at the end.
791  :raises ParseError: if the module name does not end in '_mod'.
792 
793  '''
794  def __init__(self, ast, name=None):
795 
796  if name is None:
797  # if no name is supplied then use the module name to
798  # determine the type name. The assumed convention is that
799  # the module is called <name/>_mod and the type is called
800  # <name/>_type
801  found = False
802  for statement, _ in fpapi.walk(ast):
803  if isinstance(statement, fparser1.block_statements.Module):
804  module_name = statement.name
805  found = True
806  break
807  if not found:
808  raise ParseError(
809  "Error KernelType, the file does not contain a module. "
810  "Is it a Kernel file?")
811 
812  mn_len = len(module_name)
813  if mn_len < 5:
814  raise ParseError(
815  f"Error, module name '{module_name}' is too short to have "
816  f"'_mod' as an extension. This convention is assumed.")
817  base_name = module_name.lower()[:mn_len-4]
818  extension_name = module_name.lower()[mn_len-4:mn_len]
819  if extension_name != "_mod":
820  raise ParseError(
821  f"Error, module name '{module_name}' does not have '_mod' "
822  f"as an extension. This convention is assumed.")
823  name = base_name + "_type"
824 
825  self._name_name = name
826  self._ast_ast = ast
827  self._ktype_ktype = get_kernel_metadata(name, ast)
828  # TODO #1204 since the valid form of the metadata beyond this point
829  # depends on the API, the code beyond this point should be refactored
830  # into LFRicKernMetadata and GOKernelType1p0.
831  operates_on = self.get_integer_variableget_integer_variable("operates_on")
832  # The GOcean API still uses the 'iterates_over' metadata entry
833  # although this is deprecated in the LFRic API.
834  # Validation is left to the API-specific code in either dynamo0p3.py
835  # or gocean1p0.py.
836  iterates_over = self.get_integer_variableget_integer_variable("iterates_over")
837  if operates_on:
838  self._iterates_over_iterates_over = operates_on
839  elif iterates_over:
840  self._iterates_over_iterates_over = iterates_over
841  else:
842  # We don't raise an error here - we leave it to the API-specific
843  # validation code.
844  self._iterates_over_iterates_over = None
845  # Although validation of the value given to operates_on or
846  # iterates_over is API-specific, we can check that the metadata doesn't
847  # specify both of them because that doesn't make sense.
848  if operates_on and iterates_over:
849  raise ParseError(f"The metadata for kernel '{name}' contains both "
850  f"'operates_on' and 'iterates_over'. Only one of "
851  f"these is permitted.")
852  self._procedure_procedure = KernelProcedure(self._ktype_ktype, name, ast)
853  self._inits_inits = getkerneldescriptors(name, self._ktype_ktype)
854  self._arg_descriptors_arg_descriptors = [] # this is set up by the subclasses
855 
856  @property
857  def name(self):
858  '''
859  :returns: the name of the kernel subroutine.
860  :rtype: str
861 
862  '''
863  return self._name_name
864 
865  @property
866  def iterates_over(self):
867  '''
868  :returns: the name of the iteration space supported by this kernel
869  :rtype: str
870 
871  '''
872  return self._iterates_over_iterates_over
873 
874  @property
875  def procedure(self):
876  '''
877  :returns: a kernelprocedure instance which contains a parse tree \
878  of the kernel subroutine and its name.
879  :rtype: :py:class:`psyclone.parse.kernel.KernelProcedure`
880 
881  '''
882  return self._procedure_procedure
883 
884  @property
885  def nargs(self):
886  '''
887  :returns: the number of arguments specified in the metadata.
888  :rtype: int
889 
890  '''
891  return len(self._arg_descriptors_arg_descriptors)
892 
893  @property
894  def arg_descriptors(self):
895  '''
896  :returns: a list of API-specific argument descriptors.
897  :rtype: list of API-specific specialisation of \
898  :py:class:`psyclone.kernel.Descriptor`
899 
900  '''
901  return self._arg_descriptors_arg_descriptors
902 
903  def __repr__(self):
904  return f"KernelType({self.name}, {self.iterates_over})"
905 
906  def get_integer_variable(self, name):
907  ''' Parse the kernel meta-data and find the value of the
908  integer variable with the supplied name. Return None if no
909  matching variable is found. The search is not case sensitive.
910 
911  :param str name: the name of the integer variable to find.
912 
913  :return: value of the specified integer variable (lower case) or None.
914  :rtype: str
915 
916  :raises ParseError: if the RHS of the assignment is not a Name.
917 
918  '''
919  # Ensure the Fortran2008 parser is initialised
920  _ = ParserFactory().create(std="f2008")
921  # Fortran is not case sensitive so nor is our matching
922  lower_name = name.lower()
923 
924  for statement, _ in fpapi.walk(self._ktype_ktype):
925  if isinstance(statement, fparser1.typedecl_statements.Integer):
926  # fparser only goes down to the statement level. We use
927  # fparser2 to parse the statement itself (eventually we'll
928  # use fparser2 to parse the whole thing).
929  assign = Fortran2003.Assignment_Stmt(
930  statement.entity_decls[0])
931  if str(assign.items[0]).lower() == lower_name:
932  if not isinstance(assign.items[2], Fortran2003.Name):
933  raise ParseError(
934  f"get_integer_variable: RHS of assignment is not "
935  f"a variable name: '{assign}'")
936  return str(assign.items[2]).lower()
937  return None
938 
939  def get_integer_array(self, name):
940  ''' Parse the kernel meta-data and find the values of the
941  integer array variable with the supplied name. Returns an empty list
942  if no matching variable is found. The search is not case sensitive.
943 
944  :param str name: the name of the integer array to find.
945 
946  :return: list of values (lower-case).
947  :rtype: list of str.
948 
949  :raises InternalError: if we fail to parse the LHS of the array \
950  declaration or the array constructor.
951  :raises ParseError: if the array is not of rank 1.
952  :raises ParseError: if the array extent is not specified using an \
953  integer literal.
954  :raises ParseError: if the RHS of the declaration is not an array \
955  constructor.
956  :raises InternalError: if the parse tree for the array constructor \
957  does not have the expected structure.
958  :raises ParseError: if the number of items in the array constructor \
959  does not match the extent of the array.
960 
961  '''
962  # Ensure the classes are setup for the Fortran2008 parser
963  _ = ParserFactory().create(std="f2008")
964  # Fortran is not case sensitive so nor is our matching
965  lower_name = name.lower()
966 
967  for statement, _ in fpapi.walk(self._ktype_ktype):
968  if not isinstance(statement, fparser1.typedecl_statements.Integer):
969  # This isn't an integer declaration so skip it
970  continue
971  # fparser only goes down to the statement level. We use fparser2 to
972  # parse the statement itself.
973  assign = Fortran2003.Assignment_Stmt(statement.entity_decls[0])
974  names = walk(assign.children, Fortran2003.Name)
975  if not names:
976  raise InternalError(f"Unsupported assignment statement: "
977  f"'{assign}'")
978 
979  if str(names[0]).lower() != lower_name:
980  # This is not the variable declaration we're looking for
981  continue
982 
983  if not isinstance(assign.children[0], Fortran2003.Part_Ref):
984  # Not an array declaration
985  return []
986 
987  if not isinstance(assign.children[0].children[1],
988  Fortran2003.Section_Subscript_List):
989  raise InternalError(
990  f"get_integer_array: expected array declaration to have a "
991  f"Section_Subscript_List but found "
992  f"'{type(assign.children[0].children[1]).__name__}' "
993  f"for: '{assign}'")
994 
995  dim_stmt = assign.children[0].children[1]
996  if len(dim_stmt.children) != 1:
997  raise ParseError(
998  f"get_integer_array: array must be 1D but found an array "
999  f"with {len(dim_stmt.children)} dimensions for name '"
1000  f"{name}'")
1001  if not isinstance(dim_stmt.children[0],
1002  Fortran2003.Int_Literal_Constant):
1003  raise ParseError(
1004  f"get_integer_array: array extent must be specified using "
1005  f"an integer literal but found '{dim_stmt.children[0]}' "
1006  f"for array '{name}'")
1007  # Get the declared size of the array
1008  array_extent = int(str(dim_stmt.children[0]))
1009 
1010  if not isinstance(assign.children[2],
1011  Fortran2003.Array_Constructor):
1012  raise ParseError(
1013  f"get_integer_array: RHS of assignment is not "
1014  f"an array constructor: '{assign}'")
1015  # fparser2 AST for Array_Constructor is:
1016  # Array_Constructor('[', Ac_Value_List(',', (Name('w0'),
1017  # Name('w1'))), ']')
1018  # Construct a list of the names in the array constructor
1019  names = walk(assign.children[2].children, Fortran2003.Name)
1020  if not names:
1021  raise InternalError(f"Failed to parse array constructor: "
1022  f"'{assign.items[2]}'")
1023  if len(names) != array_extent:
1024  # Ideally fparser would catch this but it isn't yet mature
1025  # enough.
1026  raise ParseError(
1027  f"get_integer_array: declared length of array '{name}' is "
1028  f"{array_extent} but constructor only contains "
1029  f"{len(names)} names: '{assign}'")
1030  return [str(name).lower() for name in names]
1031  # No matching declaration for the provided name was found
1032  return []
def create(self, builtin_names, builtin_defs_file, name=None)
Definition: kernel.py:262
def get_procedure(ast, name, modast)
Definition: kernel.py:526
def create(self, parse_tree, name=None)
Definition: kernel.py:227
def get_integer_array(self, name)
Definition: kernel.py:939
def get_integer_variable(self, name)
Definition: kernel.py:906