Reference Guide  2.5.0
algorithm.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2019-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 and N. Nobre, STFC Daresbury Lab
35 
36 '''Module that uses the Fortran parser fparser2 to parse
37 PSyclone-conformant Algorithm code.
38 
39 '''
40 
41 from collections import OrderedDict
42 
43 from fparser.two.utils import walk
44 # pylint: disable=no-name-in-module
45 from fparser.two.Fortran2003 import Main_Program, Module, \
46  Subroutine_Subprogram, Function_Subprogram, Use_Stmt, Call_Stmt, \
47  Actual_Arg_Spec, Data_Ref, Part_Ref, Char_Literal_Constant, \
48  Section_Subscript_List, Name, Real_Literal_Constant, \
49  Int_Literal_Constant, Function_Reference, Level_2_Unary_Expr, \
50  Add_Operand, Parenthesis, Structure_Constructor, Component_Spec_List, \
51  Proc_Component_Ref, Kind_Selector, Type_Declaration_Stmt, \
52  Declaration_Type_Spec, Entity_Decl, Intrinsic_Type_Spec, \
53  Data_Component_Def_Stmt, Component_Decl
54 # pylint: enable=no-name-in-module
55 
56 from psyclone.configuration import Config
57 from psyclone.errors import InternalError
58 from psyclone.parse.kernel import BuiltInKernelTypeFactory, get_kernel_ast, \
59  KernelTypeFactory
60 from psyclone.parse.utils import check_api, check_line_length, ParseError, \
61  parse_fp2
62 from psyclone.psyir.frontend.fortran import FortranReader
63 
64 # pylint: disable=too-many-statements
65 # pylint: disable=too-many-branches
66 
67 # Section 1: parse the algorithm file
68 
69 
70 def parse(alg_filename, api="", invoke_name="invoke", kernel_paths=None,
71  line_length=False):
72  '''Takes a PSyclone conformant algorithm file as input and outputs a
73  parse tree of the code contained therein and an object containing
74  information about the 'invoke' calls in the algorithm file and any
75  associated kernels within the invoke calls.
76 
77  :param str alg_filename: the file containing the algorithm \
78  specification.
79  :param str api: the PSyclone API to use when parsing the \
80  code. Defaults to empty string.
81  :param str invoke_name: the expected name of the invocation calls \
82  in the algorithm code. Defaults to "invoke".
83  :param kernel_paths: the paths to search for kernel source files \
84  (if different from the location of the algorithm source). \
85  Defaults to None.
86  :type kernel_paths: list of str or NoneType
87  :param bool line_length: a logical flag specifying whether we care \
88  about line lengths being longer than 132 characters. If so, \
89  the input (algorithm and kernel) code is checked to make sure \
90  that it conforms and an error raised if not. The default is \
91  False.
92 
93  :returns: 2-tuple consisting of the fparser2 parse tree of the \
94  Algorithm file and an object holding details of the invokes \
95  found.
96  :rtype: (:py:class:`fparser.two.Fortran2003.Program`, \
97  :py:class:`psyclone.parse.FileInfo`)
98 
99  For example:
100 
101  >>> from psyclone.parse.algorithm import parse
102  >>> ast, info = parse(SOURCE_FILE)
103 
104  '''
105  if kernel_paths is None:
106  kernel_paths = []
107  # Parsing is encapsulated in the Parser class. We keep this
108  # function for compatibility.
109  my_parser = Parser(api, invoke_name, kernel_paths, line_length)
110  return my_parser.parse(alg_filename)
111 
112 
113 # pylint: disable=too-many-instance-attributes
114 class Parser():
115  '''Supports the parsing of PSyclone conformant algorithm code within a
116  file and extraction of relevant information for any 'invoke' calls
117  contained within the code.
118 
119  :param str api: the PSyclone API to use when parsing the code.
120  :param str invoke_name: the expected name of the invocation calls in the \
121  algorithm code.
122  :param kernel_paths: the paths to search for kernel source files \
123  (if different from the location of the algorithm source). \
124  Defaults to None.
125  :type kernel_paths: list of str or NoneType
126  :param bool line_length: a logical flag specifying whether we care \
127  about line lengths being longer than 132 characters. If so, \
128  the input (algorithm and kernel) code is checked to make sure \
129  that it conforms and an error raised if not. The default is \
130  False.
131 
132  For example:
133 
134  >>> from psyclone.parse.algorithm import Parser
135  >>> parser = Parser(api="gocean1.0")
136  >>> ast, info = parser.parse(SOURCE_FILE)
137 
138  '''
139 
140  def __init__(self, api="", invoke_name="invoke", kernel_paths=None,
141  line_length=False):
142 
143  self._invoke_name_invoke_name = invoke_name
144  if kernel_paths is None:
145  self._kernel_paths_kernel_paths = []
146  else:
147  self._kernel_paths_kernel_paths = kernel_paths
148  self._line_length_line_length = line_length
149 
150  _config = Config.get()
151  if not api:
152  api = _config.default_api
153  else:
154  check_api(api)
155  self._api_api = api
156 
157  self._arg_name_to_module_name_arg_name_to_module_name = {}
158  # Dict holding a 2-tuple consisting of type and precision
159  # information for each variable declared in the algorithm
160  # file, indexed by variable name.
161  self._arg_type_defns_arg_type_defns = {}
162  self._unique_invoke_labels_unique_invoke_labels = []
163 
164  # Use the get_builtin_defs helper function to access
165  # information about the builtins supported by this API. The
166  # first argument contains the names of the builtins and the
167  # second is the file where these names are defined.
168  self._builtin_name_map, \
169  self._builtin_defs_file_builtin_defs_file = get_builtin_defs(self._api_api)
170 
171  self._alg_filename_alg_filename = None
172 
173  def parse(self, alg_filename):
174  '''Takes a PSyclone conformant algorithm file as input and outputs a
175  parse tree of the code contained therein and an object
176  containing information about the 'invoke' calls in the
177  algorithm file and any associated kernels within the invoke
178  calls. If the NEMO API is being used then the parsed code is
179  returned without any additional information about the code.
180 
181  :param str alg_filename: The file containing the algorithm code.
182 
183  :returns: 2-tuple consisting of the fparser2 parse tree of the \
184  algorithm code and an object holding details of the \
185  algorithm code and the invokes found within it, unless it \
186  is the NEMO API, where the first entry of the tuple is \
187  None and the second is the fparser2 parse tree of the \
188  code.
189  :rtype: (:py:class:`fparser.two.Fortran2003.Program`, \
190  :py:class:`psyclone.parse.FileInfo`) or (NoneType, \
191  :py:class:`fparser.two.Fortran2003.Program`)
192 
193  '''
194  self._alg_filename_alg_filename = alg_filename
195  if self._line_length_line_length:
196  # Make sure the code conforms to the line length limit.
197  check_line_length(alg_filename)
198  alg_parse_tree = parse_fp2(alg_filename)
199 
200  if self._api_api == "nemo":
201  # For this API we just parse the NEMO code and return the resulting
202  # fparser2 AST with None for the Algorithm AST.
203  return None, alg_parse_tree
204 
205  return alg_parse_tree, self.invoke_infoinvoke_info(alg_parse_tree)
206 
207  def invoke_info(self, alg_parse_tree):
208  '''Takes an fparser2 representation of a PSyclone-conformant algorithm
209  code as input and returns an object containing information
210  about the 'invoke' calls in the algorithm file and any
211  associated kernels within the invoke calls. Also captures the
212  type and precision of every variable declaration within the
213  parse tree.
214 
215  :param alg_parse_tree: the fparser2 representation of the \
216  algorithm code.
217  :type: :py:class:`fparser.two.Fortran2003.Program`
218 
219  :returns: an object holding details of the algorithm \
220  code and the invokes found within it.
221  :rtype: :py:class:`psyclone.parse.FileInfo`
222 
223  :raises ParseError: if a program, module, subroutine or \
224  function is not found in the fparser2 tree.
225  :raises InternalError: if the fparser2 tree representing the \
226  type declaration statements is not in the expected form.
227  :raises NotImplementedError: if the algorithm code contains \
228  two different datatypes with the same name.
229 
230  '''
231  # Find the first program, module, subroutine or function in the
232  # parse tree. The assumption here is that the first is the one
233  # that is required. See issue #307.
234  container_name = None
235  for child in alg_parse_tree.content:
236  if isinstance(child, (Main_Program, Module, Subroutine_Subprogram,
237  Function_Subprogram)):
238  container_name = str(child.content[0].items[1])
239  break
240 
241  if not container_name:
242  # Nothing relevant found.
243  raise ParseError(
244  "algorithm.py:parser:parse: Program, module, function or "
245  "subroutine not found in fparser2 parse tree.")
246 
247  self._unique_invoke_labels_unique_invoke_labels = []
248  self._arg_name_to_module_name_arg_name_to_module_name = OrderedDict()
249  # Dict holding a 2-tuple consisting of type and precision
250  # information for each variable declared in the algorithm
251  # file, indexed by variable name.
252  self._arg_type_defns_arg_type_defns = {}
253  invoke_calls = []
254 
255  # Find all invoke calls and capture information about
256  # them. Also find information about use statements and find
257  # all declarations within the supplied parse
258  # tree. Declarations will include the definitions of any
259  # components of derived types that are defined within the
260  # code.
261  for statement in walk(alg_parse_tree.content,
262  types=(Type_Declaration_Stmt,
263  Data_Component_Def_Stmt,
264  Use_Stmt, Call_Stmt)):
265  if isinstance(statement,
266  (Type_Declaration_Stmt, Data_Component_Def_Stmt)):
267  # Capture datatype information for the variable
268  spec = statement.children[0]
269  if isinstance(spec, Declaration_Type_Spec):
270  # This is a type declaration
271  my_type = spec.children[1].string.lower()
272  my_precision = None
273  elif isinstance(spec, Intrinsic_Type_Spec):
274  # This is an intrinsic declaration
275  my_type = spec.children[0].lower()
276  my_precision = None
277  if isinstance(spec.children[1], Kind_Selector):
278  my_precision = \
279  spec.children[1].children[1].string.lower()
280  else:
281  raise InternalError(
282  f"Expected first child of Type_Declaration_Stmt or "
283  f"Data_Component_Def_Stmt to be Declaration_Type_Spec "
284  f"or Intrinsic_Type_Spec but found "
285  f"'{type(spec).__name__}'")
286  for decl in walk(statement.children[2], (
287  Entity_Decl, Component_Decl)):
288  # Determine the variables names. Note that if a
289  # variable declaration is a component of a derived
290  # type, its name is stored 'as is'. This means
291  # that e.g. real :: a will clash with a
292  # derived-type definition if the latter has a
293  # component named 'a' and their datatypes differ.
294  my_var_name = decl.children[0].string.lower()
295  if my_var_name in self._arg_type_defns_arg_type_defns and (
296  self._arg_type_defns_arg_type_defns[my_var_name][0] != my_type or
297  self._arg_type_defns_arg_type_defns[my_var_name][1] !=
298  my_precision):
299  raise NotImplementedError(
300  f"The same symbol '{my_var_name}' is used for "
301  f"different datatypes, "
302  f"'{self._arg_type_defns[my_var_name][0]}, "
303  f"{self._arg_type_defns[my_var_name][1]}' and "
304  f"'{my_type}, {my_precision}'. This is not "
305  f"currently supported.")
306  # Store the variable name and information about its type
307  self._arg_type_defns_arg_type_defns[my_var_name] = (my_type, my_precision)
308 
309  if isinstance(statement, Use_Stmt):
310  # found a Fortran use statement
311  self.update_arg_to_module_mapupdate_arg_to_module_map(statement)
312 
313  if isinstance(statement, Call_Stmt):
314  # found a Fortran call statement
315  call_name = str(statement.items[0])
316  if call_name.lower() == self._invoke_name_invoke_name.lower():
317  # The call statement is an invoke
318  invoke_call = self.create_invoke_callcreate_invoke_call(statement)
319  invoke_calls.append(invoke_call)
320 
321  return FileInfo(container_name, invoke_calls)
322 
323  def create_invoke_call(self, statement):
324  '''Takes the part of a parse tree containing an invoke call and
325  returns an InvokeCall object which captures the required
326  information about the invoke.
327 
328  :param statement: Parse tree of the invoke call.
329  :type statement: :py:class:`fparser.two.Fortran2003.Call_Stmt`
330  :returns: An InvokeCall object which contains relevant \
331  information about the invoke call.
332  :rtype: :py:class:`psyclone.parse.algorithm.InvokeCall`
333  :raises ParseError: if more than one invoke argument contains \
334  'name=xxx'.
335  :raises ParseError: if an unknown or unsupported invoke \
336  argument is found.
337 
338  '''
339  # Extract argument list.
340  argument_list = statement.items[1].items
341 
342  invoke_label = None
343  kernel_calls = []
344 
345  for argument in argument_list:
346 
347  if isinstance(argument, Actual_Arg_Spec):
348  # This should be the invoke label.
349  if invoke_label:
350  raise ParseError(
351  f"algorithm.py:Parser():create_invoke_call: An invoke "
352  f"must contain one or zero 'name=xxx' arguments but "
353  f"found more than one in: {statement} in file "
354  f"{self._alg_filename}")
355  invoke_label = self.check_invoke_labelcheck_invoke_label(argument)
356 
357  elif isinstance(
358  argument, (Data_Ref, Part_Ref, Structure_Constructor)):
359  # This should be a kernel call.
360  kernel_call = self.create_kernel_callcreate_kernel_call(argument)
361  kernel_calls.append(kernel_call)
362 
363  else:
364  # Unknown and/or unsupported argument type
365  raise ParseError(
366  f"algorithm.py:Parser():create_invoke_call: Expecting "
367  f"argument to be of the form 'name=xxx' or a Kernel call "
368  f"but found '{argument}' in file '{self._alg_filename}'.")
369 
370  return InvokeCall(kernel_calls, name=invoke_label)
371 
372  def create_kernel_call(self, argument):
373  '''Takes the parse tree of an invoke argument containing a
374  reference to a kernel or a builtin and returns the kernel or
375  builtin object respectively which contains the required
376  information.
377 
378  :param argument: Parse tree of an invoke argument. This \
379  should contain a kernel name and associated arguments.
380  :type argument: :py:class:`fparser.two.Fortran2003.Part_Ref` or \
381  :py:class:`fparser.two.Fortran2003.Structure_Constructor`
382 
383  :returns: A builtin or coded kernel call object which contains \
384  relevant information about the Kernel.
385  :rtype: :py:class:`psyclone.parse.algorithm.KernelCall` or \
386  :py:class:`psyclone.parse.algorithm.BuiltInCall`
387 
388  '''
389  kernel_name, args = get_kernel(argument, self._alg_filename_alg_filename,
390  self._arg_type_defns_arg_type_defns)
391  if kernel_name.lower() in self._builtin_name_map:
392  # This is a builtin kernel
393  kernel_call = self.create_builtin_kernel_callcreate_builtin_kernel_call(
394  kernel_name, args)
395  else:
396  # This is a coded kernel
397  kernel_call = self.create_coded_kernel_callcreate_coded_kernel_call(
398  kernel_name, args)
399  return kernel_call
400 
401  def create_builtin_kernel_call(self, kernel_name, args):
402  '''Takes the builtin kernel name and a list of Arg objects which
403  capture information about the builtin call arguments and
404  returns a BuiltinCall instance with content specific to the
405  particular API (as specified in self._api).
406 
407  :param str kernel_name: the name of the builtin kernel being \
408  called
409  :param args: a list of 'Arg' instances containing the required \
410  information for the arguments being passed from the algorithm \
411  layer. The list order is the same as the argument order.
412  :type arg: list of :py:class:`psyclone.parse.algorithm.Arg`
413  :returns: a BuiltInCall instance with information specific to \
414  the API.
415  :rtype: :py:class:`psyclone.parse.algorithm.BuiltInCall`
416  :raises ParseError: if the builtin is specified in a use \
417  statement in the algorithm layer
418 
419  '''
420  if kernel_name.lower() in self._arg_name_to_module_name_arg_name_to_module_name:
421  raise ParseError(
422  f"A built-in cannot be named in a use statement but "
423  f"'{kernel_name}' is used from module "
424  f"'{self._arg_name_to_module_name[kernel_name.lower()]}' in "
425  f"file {self._alg_filename}")
426 
427  return BuiltInCall(BuiltInKernelTypeFactory(api=self._api_api).create(
428  self._builtin_name_map.keys(), self._builtin_defs_file_builtin_defs_file,
429  name=kernel_name.lower()), args)
430 
431  def create_coded_kernel_call(self, kernel_name, args):
432  '''Takes a coded kernel name and a list of Arg objects which
433  capture information about the coded call arguments and
434  returns a KernelCall instance with content specific to the
435  particular API (as specified in self._api).
436 
437  :param str kernel_name: the name of the coded kernel being \
438  called
439  :param args: a list of 'Arg' instances containing the required \
440  information for the arguments being passed from the algorithm \
441  layer. The list order is the same as the argument order.
442  :type arg: list of :py:class:`psyclone.parse.algorithm.Arg`
443  :returns: a KernelCall instance with information specific to \
444  the API.
445  :rtype: :py:class:`psyclone.parse.algorithm.KernelCall`
446  :raises ParseError: if the kernel is not specified in a use \
447  statement in the algorithm layer
448 
449  '''
450  try:
451  module_name = self._arg_name_to_module_name_arg_name_to_module_name[kernel_name.lower()]
452  except KeyError as info:
453  message = (
454  f"kernel call '{kernel_name.lower()}' must either be named in "
455  f"a use statement (found "
456  f"{list(self._arg_name_to_module_name.values())}) or be a "
457  f"recognised built-in (one of "
458  f"'{list(self._builtin_name_map.keys())}' for "
459  f"this API)")
460  raise ParseError(message) from info
461 
462  modast = get_kernel_ast(module_name, self._alg_filename_alg_filename,
463  self._kernel_paths_kernel_paths, self._line_length_line_length)
464  return KernelCall(module_name,
465  KernelTypeFactory(api=self._api_api).create(
466  modast, name=kernel_name), args)
467 
468  def update_arg_to_module_map(self, statement):
469  '''Takes a use statement and adds its contents to the internal
470  arg_name_to_module_name map. This map associates names
471  specified in the 'only' list with the corresponding use name.
472 
473  :param statement: A use statement
474  :type statement: :py:class:`fparser.two.Fortran2003.Use_Stmt`
475  :raises InternalError: if the statement being passed is not an \
476  fparser use statement.
477 
478  '''
479  # make sure statement is a use
480  if not isinstance(statement, Use_Stmt):
481  raise InternalError(
482  f"algorithm.py:Parser:update_arg_to_module_map: Expected "
483  f"a use statement but found instance of "
484  f"'{type(statement)}'.")
485 
486  use_name = str(statement.items[2])
487 
488  # Extract 'only' list.
489  if statement.items[4]:
490  only_list = statement.items[4].items
491  else:
492  only_list = []
493 
494  for item in only_list:
495  self._arg_name_to_module_name_arg_name_to_module_name[str(item).lower()] = use_name
496 
497  def check_invoke_label(self, argument):
498  '''Takes the parse tree of an invoke argument containing an invoke
499  label. Raises an exception if this label has already been used
500  by another invoke in the same algorithm code. If all is well
501  it returns the label as a string.
502 
503  :param argument: Parse tree of an invoke argument. This \
504  should contain a "name=xxx" argument.
505  :type argument: :py:class:`fparser.two.Actual_Arg_Spec`
506  :returns: the label as a string.
507  :rtype: str
508  :raises ParseError: if this label has already been used by \
509  another invoke in this algorithm code.
510 
511  '''
512  invoke_label = get_invoke_label(argument, self._alg_filename_alg_filename)
513  if invoke_label in self._unique_invoke_labels_unique_invoke_labels:
514  raise ParseError(
515  f"Found multiple named invoke()'s with the same "
516  f"label ('{invoke_label}') when parsing {self._alg_filename}")
517  self._unique_invoke_labels_unique_invoke_labels.append(invoke_label)
518  return invoke_label
519 
520 # pylint: enable=too-many-arguments
521 # pylint: enable=too-many-instance-attributes
522 
523 # Section 2: Support functions
524 
525 
526 def get_builtin_defs(api):
527  '''
528  Get the names of the supported built-in operations and the file
529  containing the associated meta-data for the supplied API
530 
531  :param str api: the specified PSyclone API.
532  :returns: a 2-tuple containing a dictionary of the supported \
533  built-ins and the filename where these built-ins are specified.
534  :rtype: (dict, str)
535 
536  '''
537 
538  # Check that the supplied API is valid
539  check_api(api)
540 
541  # pylint: disable=import-outside-toplevel
542  if api == "dynamo0.3":
543  from psyclone.domain.lfric.lfric_builtins import BUILTIN_MAP \
544  as builtins
546  BUILTIN_DEFINITIONS_FILE as fname
547  else:
548  # We don't support any built-ins for this API
549  builtins = {}
550  fname = None
551  return builtins, fname
552 
553 
554 def get_invoke_label(parse_tree, alg_filename, identifier="name"):
555  '''Takes an invoke argument contained in the parse_tree argument and
556  returns the label specified within it.
557 
558  :param parse_tree: Parse tree of an invoke argument. This should \
559  contain a "name=xxx" argument.
560  :type parse_tree: :py:class:`fparser.two.Actual_Arg_Spec`
561  :param str alg_filename: The file containing the algorithm code.
562  :param str identifier: An optional name used to specify a named argument. \
563  Defaults to 'name'.
564 
565  :returns: the label as a lower-cased string.
566  :rtype: str
567 
568  :except InternalError: if the form of the argument is not what was \
569  expected.
570  :except InternalError: if the number of items contained in the \
571  argument is not what was expected.
572  :except ParseError: if the name used for the named argument does \
573  not match what was expected.
574  :except ParseError: if the label is not specified as a string.
575  :except ParseError: if the label is not a valid Fortran name.
576 
577  '''
578  if not isinstance(parse_tree, Actual_Arg_Spec):
579  raise InternalError(
580  f"algorithm.py:Parser:get_invoke_label: Expected a Fortran "
581  f"argument of the form name=xxx but found instance of "
582  f"'{type(parse_tree)}'.")
583 
584  if len(parse_tree.items) != 2:
585  raise InternalError(
586  f"algorithm.py:Parser:get_invoke_label: Expected the Fortran "
587  f"argument to have two items but found "
588  f"'{len(parse_tree.items)}'.")
589 
590  ident = str(parse_tree.items[0])
591  if ident.lower() != identifier.lower():
592  raise ParseError(
593  f"algorithm.py:Parser:get_invoke_label: Expected named identifier "
594  f"to be '{identifier.lower()}' but found '{ident.lower()}'")
595 
596  if not isinstance(parse_tree.items[1], Char_Literal_Constant):
597  raise ParseError(
598  f"algorithm.py:Parser:get_invoke_label: The (optional) name of an "
599  f"invoke must be specified as a string, but found "
600  f"{parse_tree.items[1]} in {alg_filename}")
601 
602  invoke_label = parse_tree.items[1].items[0]
603  invoke_label = invoke_label.lower()
604  invoke_label = invoke_label.strip()
605  if invoke_label[0] == '"' and invoke_label[-1] == '"' or \
606  invoke_label[0] == "'" and invoke_label[-1] == "'":
607  invoke_label = invoke_label[1:-1]
608 
609  try:
610  if invoke_label:
611  FortranReader.validate_name(invoke_label)
612  # We store any name as lowercase.
613  invoke_label = invoke_label.lower()
614  except (TypeError, ValueError) as err:
615  raise (
616  ParseError(
617  f"algorithm.py:Parser:get_invoke_label the (optional) name of "
618  f"an invoke must be a string containing a valid Fortran name "
619  f"(with no whitespace) but got '{invoke_label}' in file "
620  f"{alg_filename}")) from err
621 
622  return invoke_label
623 
624 
625 def get_kernel(parse_tree, alg_filename, arg_type_defns):
626  '''Takes the parse tree of an invoke kernel argument and returns the
627  name of the kernel and a list of Arg instances which capture the
628  relevant information about the arguments associated with the
629  kernel.
630 
631  :param parse_tree: parse tree of an invoke argument. This \
632  should contain a kernel name and associated arguments.
633  :type parse_tree: :py:class:`fparser.two.Fortran2003.Part_Ref` or \
634  :py:class:`fparser.two.Fortran2003.Structure_Constructor`
635  :param str alg_filename: The file containing the algorithm code.
636 
637  :param arg_type_defns: dictionary holding a 2-tuple consisting of \
638  type and precision information for each variable declared in \
639  the algorithm layer, indexed by variable name.
640  :type arg_type_defns: dict[str] = (str, str or NoneType)
641 
642  :returns: a 2-tuple with the name of the kernel being called and a \
643  list of 'Arg' instances containing the required information for \
644  the arguments being passed from the algorithm layer. The list \
645  order is the same as the argument order.
646  :rtype: (str, list of :py:class:`psyclone.parse.algorithm.Arg`)
647 
648  :raises InternalError: if the parse tree is of the wrong type.
649  :raises InternalError: if Part_Ref or Structure_Constructor do not \
650  have two children.
651  :raises InternalError: if Proc_Component_Ref has a child with an \
652  unexpected type.
653  :raises InternalError: if Data_Ref has a child with an unexpected \
654  type.
655  :raises NotImplementedError: if an expression contains a variable.
656  :raises InternalError: if an unsupported argument format is found.
657 
658  '''
659  # pylint: disable=too-many-branches
660  if not isinstance(parse_tree, (Part_Ref, Structure_Constructor)):
661  raise InternalError(
662  f"algorithm.py:get_kernel: Expected a parse tree (type Part_Ref "
663  f"or Structure_Constructor) but found instance of "
664  f"'{type(parse_tree)}'.")
665 
666  if len(parse_tree.items) != 2:
667  raise InternalError(
668  f"algorithm.py:get_kernel: Expected Part_Ref or "
669  f"Structure_Constructor to have 2 children "
670  f"but found {len(parse_tree.items)}.")
671 
672  kernel_name = str(parse_tree.items[0])
673 
674  # Extract argument list. This can be removed when fparser#211 is fixed.
675  argument_list = []
676  if isinstance(parse_tree.items[1],
677  (Section_Subscript_List, Component_Spec_List)):
678  argument_list = parse_tree.items[1].items
679  else:
680  # Expecting a single entry rather than a list
681  argument_list = [parse_tree.items[1]]
682 
683  arguments = []
684  for argument in argument_list:
685  if isinstance(argument, (Real_Literal_Constant, Int_Literal_Constant)):
686  # A simple constant e.g. 1.0, or 1_i_def
687  precision = argument.children[1]
688  if precision:
689  precision = str(precision)
690  if isinstance(argument, Real_Literal_Constant):
691  datatype = ("real", precision)
692  else:
693  datatype = ("integer", precision)
694  arguments.append(Arg('literal', argument.tostr().lower(),
695  datatype=datatype))
696  elif isinstance(argument, Name):
697  # A simple variable e.g. arg
698  full_text = str(argument).lower()
699  var_name = full_text
700  datatype = arg_type_defns.get(var_name)
701  arguments.append(Arg('variable', full_text, varname=var_name,
702  datatype=datatype))
703  elif isinstance(argument, Part_Ref):
704  # An indexed variable e.g. arg(n)
705  full_text = argument.tostr().lower()
706  var_name = str(argument.items[0]).lower()
707  datatype = arg_type_defns.get(var_name)
708  arguments.append(Arg('indexed_variable', full_text,
709  varname=var_name, datatype=datatype))
710  elif isinstance(argument, Function_Reference):
711  # A function reference e.g. func(). The datatype of this
712  # function is not determined so datatype in Arg is None.
713  full_text = argument.tostr().lower()
714  designator = argument.items[0]
715  lhs = designator.items[0]
716  lhs = create_var_name(lhs)
717  rhs = str(designator.items[2])
718  var_name = f"{lhs}_{rhs}"
719  var_name = var_name.lower()
720  arguments.append(Arg('indexed_variable', full_text,
721  varname=var_name))
722  elif isinstance(argument, (Data_Ref, Proc_Component_Ref)):
723  # A structure access e.g. a % b or self % c
724  if isinstance(argument, Proc_Component_Ref):
725  if isinstance(argument.children[2], Name):
726  arg = argument.children[2].string.lower()
727  else:
728  # It does not appear to be possible to get to here
729  # as an array (e.g. self%a(10)) is not treated as
730  # being a Proc_Component_Ref by fparser2 and
731  # Data_Ref otherwise always has a Name on the rhs
732  # (3rd argument).
733  raise InternalError(
734  f"The third argument to to a Proc_Component_Ref is "
735  f"expected to be a Name, but found "
736  f"'{type(argument.children[2]).__name__}'.")
737  elif isinstance(argument, Data_Ref):
738  rhs_node = argument.children[-1]
739  if isinstance(rhs_node, Part_Ref):
740  rhs_node = rhs_node.children[0]
741  if not isinstance(rhs_node, Name):
742  raise InternalError(
743  f"The last child of a Data_Ref is expected to be "
744  f"a Name or a Part_Ref whose first child is a "
745  f"Name, but found '{type(rhs_node).__name__}'.")
746  arg = rhs_node.string.lower()
747  datatype = arg_type_defns.get(arg)
748  full_text = argument.tostr().lower()
749  var_name = create_var_name(argument).lower()
750  collection_type = None
751  if not datatype and isinstance(argument, Data_Ref):
752  # This could be a collection of some sort.
753  # Find the name of the parent type.
754  collection = argument.children[-2]
755  if isinstance(collection, Part_Ref):
756  collection = collection.children[0]
757  if isinstance(collection, Name):
758  collection_type = arg_type_defns.get(
759  collection.tostr().lower())
760  if collection_type:
761  arguments.append(
762  Arg('collection', full_text,
763  varname=var_name, datatype=collection_type))
764  else:
765  arguments.append(Arg('variable', full_text,
766  varname=var_name, datatype=datatype))
767  elif isinstance(argument, (Level_2_Unary_Expr, Add_Operand,
768  Parenthesis)):
769  # An expression e.g. -1, 1*n, ((1*n)/m). Note, for some
770  # reason Add_Operation represents binary expressions in
771  # fparser2. Walk the tree to look for an argument.
772  if walk(argument, Name):
773  raise NotImplementedError(
774  f"algorithm.py:get_kernel: Expressions containing "
775  f"variables are not yet supported '{type(argument)}', "
776  f"value '{str(argument)}', kernel '{parse_tree}' in "
777  f"file '{alg_filename}'.")
778  # This is a literal so store the full expression as a
779  # string.
780  candidate_datatype = None
781  for literal in walk(
782  # Determine datatype and precision.
783  argument, (Real_Literal_Constant,
784  Int_Literal_Constant)):
785  precision = literal.children[1]
786  if precision:
787  precision = str(precision)
788  if isinstance(literal, Real_Literal_Constant):
789  datatype = ("real", precision)
790  else: # it's an Int_Literal_Constant
791  datatype = ("integer", precision)
792 
793  if not candidate_datatype:
794  # This is the first candidate
795  candidate_datatype = datatype
796  elif candidate_datatype != datatype:
797  raise NotImplementedError(
798  f"Found two non-matching literals within an "
799  f"expression ('{str(argument)}') passed into an "
800  f"invoke from the algorithm layer. '{datatype}' and "
801  f"'{candidate_datatype}' do not match. This is not "
802  f"supported in PSyclone.")
803  arguments.append(Arg('literal', argument.tostr().lower(),
804  datatype=datatype))
805  else:
806  raise InternalError(
807  f"algorithm.py:get_kernel: Unsupported argument structure "
808  f"'{type(argument)}', value '{str(argument)}', kernel "
809  f"'{parse_tree}' in file '{alg_filename}'.")
810 
811  return kernel_name, arguments
812 
813 
814 def create_var_name(arg_parse_tree):
815  '''Creates a valid variable name from an argument that optionally
816  includes brackets and potentially dereferences using '%'.
817 
818  :param arg_parse_tree: the input argument. Contains braces and \
819  potentially dereferencing. e.g. a%b(c).
820  :type arg_parse_tree: :py:class:`fparser.two.Fortran2003.Name` or \
821  :py:class:`fparser.two.Fortran2003.Data_Ref` or \
822  :py:class:`fparser.two.Fortran2003.Part_Ref` or \
823  :py:class:`fparser.two.Fortran2003.Proc_Component_Ref`
824 
825  :returns: a valid variable name.
826  :rtype: str
827 
828  :raises InternalError: if unrecognised fparser content is found.
829 
830  '''
831  tree = arg_parse_tree
832  if isinstance(tree, Name):
833  return str(tree)
834  if isinstance(tree, Part_Ref):
835  return str(tree.items[0])
836  if isinstance(tree, Proc_Component_Ref):
837  # Proc_Component_Ref is of the form 'variable %
838  # proc-name'. Its RHS (proc-name) is always a Name but its
839  # LHS (variable) could be more complex, so call the function
840  # again for the LHS.
841  return f"{create_var_name(tree.items[0])}_{tree.items[2]}"
842  if isinstance(tree, Data_Ref):
843  component_names = []
844  for item in tree.items:
845  if isinstance(item, (Data_Ref, Part_Ref)):
846  component_names.append(str(item.items[0]))
847  elif isinstance(item, Name):
848  component_names.append(str(item))
849  else:
850  raise InternalError(
851  f"algorithm.py:create_var_name unrecognised structure "
852  f"'{type(item)}' in '{type(tree)}'.")
853  return "_".join(component_names)
854  raise InternalError(
855  f"algorithm.py:create_var_name unrecognised structure '{type(tree)}'")
856 
857 # Section 3: Classes holding algorithm information.
858 
859 
860 class FileInfo():
861  '''Captures information about the algorithm file and the invoke calls
862  found within the contents of the file.
863 
864  :param str name: the name of the algorithm program unit (program, \
865  module, subroutine or function)
866  :param calls: information about the invoke calls in the algorithm code.
867  :type calls: list of :py:class:`psyclone.parse.algorithm.InvokeCall`
868 
869  '''
870 
871  def __init__(self, name, calls):
872  self._name_name = name
873  self._calls_calls = calls
874 
875  @property
876  def name(self):
877  '''
878  :returns: the name of the algorithm program unit
879  :rtype: str
880 
881  '''
882  return self._name_name
883 
884  @property
885  def calls(self):
886  '''
887  :returns: information about invoke calls
888  :rtype: list of :py:class:`psyclone.parse.algorithm.InvokeCall`
889 
890  '''
891  return self._calls_calls
892 
893 
894 class InvokeCall():
895  '''Keeps information about an individual invoke call.
896 
897  :param kcalls: Information about the kernels specified in the \
898  invoke.
899  :type kcalls: list of \
900  :py:class:`psyclone.parse.algorithm.KernelCall` or \
901  :py:class:`psyclone.parse.algorithm.BuiltInCall`
902  :param str name: An optional name to call the transformed invoke \
903  call. This defaults to None.
904  :param str invoke_name: the name that is used to indicate an invoke \
905  call. This defaults to 'invoke'.
906 
907  '''
908 
909  def __init__(self, kcalls, name=None, invoke_name="invoke"):
910  self._kcalls_kcalls = kcalls
911  if name:
912  # Prefix the name with invoke_name + '_" unless it already
913  # starts with that ...
914  if not name.lower().startswith(f"{invoke_name}_"):
915  self._name_name = f"{invoke_name}_{name.lower()}"
916  else:
917  self._name_name = name.lower()
918  else:
919  self._name_name = None
920 
921  @property
922  def name(self):
923  '''
924  :returns: the name of this invoke call
925  :rtype: str
926 
927  '''
928  return self._name_name
929 
930  @property
931  def kcalls(self):
932  '''
933  :returns: the list of kernel calls in this invoke call
934  :rtype: list of \
935  :py:class:`psyclone.parse.algorithm.KernelCall` or \
936  :py:class:`psyclone.parse.algorithm.BuiltInCall`
937 
938  '''
939  return self._kcalls_kcalls
940 
941 
942 class ParsedCall():
943  '''Base class for information about a user-supplied or built-in
944  kernel.
945 
946  :param ktype: information about a kernel or builtin. Provides \
947  access to the PSyclone description metadata and the code if it \
948  exists.
949  :type ktype: API-specific specialisation of \
950  :py:class:`psyclone.parse.kernel.KernelType`
951  :param args: a list of Arg instances which capture the relevant \
952  information about the arguments associated with the call to the \
953  kernel or builtin.
954  :type args: list of :py:class:`psyclone.parse.algorithm.Arg`
955 
956  '''
957  def __init__(self, ktype, args):
958  self._ktype_ktype = ktype
959  self._args_args = args
960  if len(self._args_args) < self._ktype_ktype.nargs:
961  # we cannot test for equality here as API's may have extra
962  # arguments passed in from the algorithm layer (e.g. 'QR'
963  # in dynamo0.3), but we do expect there to be at least the
964  # same number of real arguments as arguments specified in
965  # the metadata.
966  raise ParseError(
967  f"Kernel '{self._ktype.name}' called from the algorithm layer "
968  f"with an insufficient number of arguments as specified by "
969  f"the metadata. Expected at least '{self._ktype.nargs}' but "
970  f"found '{len(self._args)}'.")
971  self._module_name_module_name = None
972 
973  @property
974  def ktype(self):
975  '''
976  :returns: information about a kernel or builtin. Provides \
977  access to the PSyclone description metadata and the code if it \
978  exists.
979  :rtype: API-specific specialisation of \
980  :py:class:`psyclone.parse.kernel.KernelType`
981 
982  '''
983  return self._ktype_ktype
984 
985  @property
986  def args(self):
987  '''
988  :returns: a list of Arg instances which capture the relevant \
989  information about the arguments associated with the call to the \
990  kernel or builtin
991  :rtype: list of :py:class:`psyclone.parse.algorithm.Arg`
992 
993  '''
994  return self._args_args
995 
996  @property
997  def module_name(self):
998  '''This name is assumed to be set by the subclasses.
999 
1000  :returns: the name of the module containing the kernel code.
1001  :rtype: str
1002 
1003  '''
1004  return self._module_name_module_name
1005 
1006 
1008  '''Store information about a user-supplied (coded) kernel. Specialises
1009  the generic ParsedCall class adding a module name value and a
1010  type for distinguishing this class.
1011 
1012  :param str module_name: the name of the kernel module.
1013  :param ktype: information about the kernel. Provides access to the \
1014  PSyclone description metadata and the code.
1015  :type ktype: API-specific specialisation of \
1016  :py:class:`psyclone.parse.kernel.KernelType`
1017  :param args: a list of Arg instances which capture the relevant \
1018  information about the arguments associated with the call to the \
1019  kernel.
1020  :type arg: list of :py:class:`psyclone.parse.algorithm.Arg`
1021 
1022  '''
1023  def __init__(self, module_name, ktype, args):
1024  ParsedCall.__init__(self, ktype, args)
1025  self._module_name_module_name_module_name = module_name
1026 
1027  @property
1028  def type(self):
1029  '''Specifies that this is a kernel call.
1030 
1031  :returns: the type of call as a string.
1032  :rtype: str
1033 
1034  '''
1035  return "kernelCall"
1036 
1037  def __repr__(self):
1038  return f"KernelCall('{self.ktype.name}', {self.args})"
1039 
1040 
1042  '''Store information about a system-supplied (builtin)
1043  kernel. Specialises the generic ParsedCall class adding a function
1044  name method (the name of the builtin) and a type for
1045  distinguishing this class.
1046 
1047  :param ktype: information about this builtin. Provides \
1048  access to the PSyclone description metadata.
1049  :type ktype: API-specific specialisation of \
1050  :py:class:`psyclone.parse.kernel.KernelType`
1051  :param args: a list of Arg instances which capture the relevant \
1052  information about the arguments associated with the call to the \
1053  kernel or builtin
1054  :type args: list of :py:class:`psyclone.parse.algorithm.Arg`
1055 
1056  '''
1057  def __init__(self, ktype, args):
1058  ParsedCall.__init__(self, ktype, args)
1059  self._func_name_func_name = ktype.name
1060 
1061  @property
1062  def func_name(self):
1063  '''
1064  :returns: the name of this builtin.
1065  :rtype: str
1066 
1067  '''
1068  return self._func_name_func_name
1069 
1070  @property
1071  def type(self):
1072  '''Specifies that this is a builtin call.
1073 
1074  :returns: the type of call as a string.
1075  :rtype: str
1076 
1077  '''
1078  return "BuiltInCall"
1079 
1080  def __repr__(self):
1081  return f"BuiltInCall('{self.ktype.name}', {self.args})"
1082 
1083 
1084 class Arg():
1085  '''Description of an argument as obtained from parsing kernel or
1086  builtin arguments within invokes in a PSyclone algorithm code.
1087 
1088  :param str form: describes whether the argument is a literal \
1089  value, standard variable or indexed variable. Supported options \
1090  are specified in the local form_options list.
1091  :param str text: the original Fortran text of the argument.
1092  :param varname: the extracted variable name from the text if the \
1093  form is not literal otherwise it is set to None. This is optional \
1094  and defaults to None.
1095  :value varname: str or NoneType
1096  :param datatype: a tuple containing information about the datatype \
1097  and precision of the argument, or None if no information is \
1098  available. Defaults to None.
1099  :type datatype: (str, str or NoneType) or NoneType
1100 
1101  :raises InternalError: if the form argument is not one one of the \
1102  supported types as specified in the local form_options list.
1103 
1104  '''
1105  form_options = ["literal", "variable", "indexed_variable", "collection"]
1106 
1107  def __init__(self, form, text, varname=None, datatype=None):
1108  self._form_form = form
1109  self._text_text = text
1110  self._varname_varname = varname
1111  if form not in Arg.form_options:
1112  raise InternalError(
1113  f"algorithm.py:Alg:__init__: Unknown arg type provided. "
1114  f"Expected one of {str(Arg.form_options)} but found "
1115  f"'{form}'.")
1116  # A tuple containing information about the datatype and
1117  # precision of this argument, or None if there is none.
1118  self._datatype_datatype = datatype
1119 
1120  def __str__(self):
1121  return (f"Arg(form='{self._form}',text='{self._text}',"
1122  f"varname='{self._varname}')")
1123 
1124  @property
1125  def form(self):
1126  '''
1127  :returns: a string indicating what type of variable this \
1128  is. Supported options are specified in the local form_options \
1129  list.
1130  :rtype: str
1131 
1132  '''
1133  return self._form_form
1134 
1135  @property
1136  def text(self):
1137  '''
1138  :returns: the original Fortran text of the argument.
1139  :rtype: str
1140 
1141  '''
1142  return self._text_text
1143 
1144  @property
1145  def varname(self):
1146  '''
1147  :returns: the extracted variable name from the text if the \
1148  form is not literal and None otherwise
1149  :rtype: str or NoneType
1150 
1151  '''
1152  return self._varname_varname
1153 
1154  @varname.setter
1155  def varname(self, value):
1156  '''Allows the setting or re-setting of the variable name value.
1157 
1158  :param str value: the new variable name
1159 
1160  '''
1161  self._varname_varname = value
1162 
1163  def is_literal(self):
1164  ''' Indicates whether this argument is a literal or not.
1165 
1166  :returns: True if this argument is a literal and False otherwise.
1167  :rtype: bool
1168 
1169  '''
1170  return self._form_form == "literal"
1171 
1172 
1173 __all__ = ["parse", "Parser", "get_builtin_defs", "get_invoke_label",
1174  "get_kernel", "create_var_name", "FileInfo", "InvokeCall",
1175  "ParsedCall", "KernelCall", "BuiltInCall", "Arg"]
def create_coded_kernel_call(self, kernel_name, args)
Definition: algorithm.py:431
def update_arg_to_module_map(self, statement)
Definition: algorithm.py:468
def invoke_info(self, alg_parse_tree)
Definition: algorithm.py:207
def check_invoke_label(self, argument)
Definition: algorithm.py:497
def create_builtin_kernel_call(self, kernel_name, args)
Definition: algorithm.py:401
def parse(self, alg_filename)
Definition: algorithm.py:173
def create_invoke_call(self, statement)
Definition: algorithm.py:323
def create_kernel_call(self, argument)
Definition: algorithm.py:372