Reference Guide  2.5.0
loop.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 # I. Kavcic, Met Office
36 # J. Henrichs, Bureau of Meteorology
37 # Modified A. B. G. Chalk, STFC Daresbury Lab
38 # -----------------------------------------------------------------------------
39 
40 ''' This module contains the Loop node implementation.'''
41 
42 from psyclone.psyir.nodes.datanode import DataNode
43 from psyclone.psyir.nodes.statement import Statement
44 from psyclone.psyir.nodes.routine import Routine
45 from psyclone.psyir.nodes import Schedule
46 from psyclone.psyir.symbols import ScalarType, DataSymbol
47 from psyclone.core import AccessType, Signature
48 from psyclone.errors import InternalError, GenerationError
49 from psyclone.f2pygen import DeclGen, PSyIRGen, UseGen
50 
51 
52 class Loop(Statement):
53  # pylint: disable=too-many-instance-attributes
54  '''Node representing a loop within the PSyIR. It has 4 mandatory children:
55  the first one represents the loop lower bound, the second one represents
56  the loop upper bound, the third one represents the step value and the
57  fourth one is always a PSyIR Schedule node containing the statements inside
58  the loop body.
59 
60  (Note: Loop only represents the equivalent to Fortran counted do loops.
61  This means the loop is bounded by start/stop/step expressions evaluated
62  before the loop starts. See WhileLoop for while loops, including the
63  Fortran do while and do loop with no condition.)
64 
65  :param variable: optional reference to the loop iterator \
66  variable. Defaults to None.
67  :type variable: Optional[:py:class:`psyclone.psyir.symbols.DataSymbol`]
68  :param annotations: One or more labels that provide additional information\
69  about the node (primarily relating to the input code that it was \
70  created from).
71  :type annotations: Optional[List[str]]
72  :param kwargs: additional keyword arguments provided to the PSyIR node.
73  :type kwargs: unwrapped dict.
74 
75  :raises InternalError: if the 'was_single_stmt' annotation is supplied \
76  without the 'was_where' annotation.
77 
78  '''
79  valid_annotations = ('was_where', 'was_single_stmt', 'chunked')
80  # Textual description of the node.
81  _children_valid_format = "DataNode, DataNode, DataNode, Schedule"
82  _text_name = "Loop"
83  _colour = "red"
84 
85  # Set of rules that give a loop a certain loop_type by inspecting
86  # its variable name
87  _loop_type_inference_rules = {}
88 
89  def __init__(self, variable=None, annotations=None, **kwargs):
90  # Although the base class checks on the annotations individually, we
91  # need to do further checks here
92  if annotations:
93  if 'was_single_stmt' in annotations and \
94  'was_where' not in annotations:
95  raise InternalError(
96  f"A Loop with the 'was_single_stmt' annotation "
97  f"must also have the 'was_where' annotation but"
98  f" got: {annotations}")
99 
100  super().__init__(self, annotations=annotations, **kwargs)
101  # Call the variable setter for error checking
102  self._variable_variable = None
103  if variable is not None:
104  self.variablevariablevariablevariable = variable
105 
106  def __eq__(self, other):
107  '''
108  Checks whether two nodes are equal. Two Loop nodes are equal
109  if they have the same iteration variable and their children are
110  equal.
111 
112  :param object other: the object to check equality to.
113 
114  :returns: whether other is equal to self.
115  :rtype: bool
116  '''
117  is_eq = super().__eq__(other)
118  # Similar to Reference equality, it is enough to compare the name
119  # since if the same-named symbols represent the same is already
120  # done in their respective scope symbol_table equality check.
121  is_eq = is_eq and self.variablevariablevariablevariable.name == other.variable.name
122 
123  return is_eq
124 
125  @property
126  def loop_type(self):
127  '''
128  :returns: the type of this loop.
129  :rtype: str
130  '''
131  return self._loop_type_inference_rules_loop_type_inference_rules_loop_type_inference_rules.get(self.variablevariablevariablevariable.name, None)
132 
133  @classmethod
135  '''
136  Specify the rules that define a loop type by inspecting its variable,
137  name. This affects all instances of the Loop class. For example:
138 
139  .. code-block::
140 
141  rules = {
142  "lon": {"variable": "ji"},
143  "lat": {"variable": "jj"}
144  }
145 
146  :param rules: new set of rules for inferring loop_types.
147  :type rules: dict[str, dict[str, str]]
148  '''
149  if rules is None:
150  cls._loop_type_inference_rules_loop_type_inference_rules_loop_type_inference_rules = {}
151  return
152 
153  # Check that the provided rules have the right format
154  if not isinstance(rules, dict):
155  raise TypeError(f"The rules argument must be of type 'dict' but "
156  f"found '{type(rules)}'.")
157  for key, rule in rules.items():
158  if not isinstance(key, str):
159  raise TypeError(f"The rules keys must be of type 'str' but "
160  f"found '{type(key)}'.")
161  if not isinstance(rule, dict):
162  raise TypeError(f"The rules values must be of type 'dict' but "
163  f"found '{type(rule)}'.")
164  for rkey, value in rule.items():
165  if not isinstance(rkey, str) or not isinstance(value, str):
166  raise TypeError(
167  f"All the values of the rule definition must be "
168  f"of type 'str' but found '{rule}'.")
169  if rkey != "variable":
170  raise TypeError(f"Currently only the 'variable' rule key"
171  f" is accepted, but found: '{rkey}'.")
172  if "variable" not in rule:
173  raise TypeError(f"A rule must at least have a 'variable' field"
174  f" to specify the loop variable name that "
175  f"defines this loop_type, but the rule for "
176  f"'{key}' does not have it.")
177 
178  # Convert the rules to a dictionary with variable as a key
179  inference_rules = {}
180  for key, rule in rules.items():
181  inference_rules[rule["variable"]] = key
182  cls._loop_type_inference_rules_loop_type_inference_rules_loop_type_inference_rules = inference_rules
183 
184  @staticmethod
185  def _check_variable(variable):
186  '''The loop variable should be a scalar integer. Check that this is
187  the case and raise an exception if not.
188 
189  :param variable: the loop iterator.
190  :type variable: :py:class:`psyclone.psyir.symbols.DataSymbol`
191 
192  :raises GenerationError: if the supplied variable is not a \
193  scalar integer.
194 
195  '''
196  try:
197  variable_name = f"'{variable.name}'"
198  except AttributeError:
199  variable_name = "property"
200  if not isinstance(variable, DataSymbol):
201  raise GenerationError(
202  f"variable {variable_name} in Loop class should be a "
203  f"DataSymbol but found '{type(variable).__name__}'.")
204  if not isinstance(variable.datatype, ScalarType):
205  raise GenerationError(
206  f"variable {variable_name} in Loop class should be a "
207  f"ScalarType but found '{type(variable.datatype).__name__}'.")
208  if variable.datatype.intrinsic != ScalarType.Intrinsic.INTEGER:
209  raise GenerationError(
210  f"variable {variable_name} in Loop class should be a "
211  f"scalar integer but found "
212  f"'{variable.datatype.intrinsic.name}'.")
213 
214  @staticmethod
215  def _validate_child(position, child):
216  '''
217  :param int position: the position to be validated.
218  :param child: a child to be validated.
219  :type child: :py:class:`psyclone.psyir.nodes.Node`
220 
221  :return: whether the given child and position are valid for this node.
222  :rtype: bool
223 
224  '''
225  return (position in (0, 1, 2) and isinstance(child, DataNode)) or (
226  position == 3 and isinstance(child, Schedule))
227 
228  @classmethod
229  def create(cls, variable, start, stop, step, children):
230  # pylint: disable=too-many-arguments
231  '''Create a Loop instance given valid instances of a variable,
232  start, stop and step nodes, and a list of child nodes for the
233  loop body.
234 
235  :param variable: the PSyIR node containing the variable \
236  of the loop iterator.
237  :type variable: :py:class:`psyclone.psyir.symbols.DataSymbol`
238  :param start: the PSyIR node determining the value for the \
239  start of the loop.
240  :type start: :py:class:`psyclone.psyir.nodes.Node`
241  :param end: the PSyIR node determining the value for the end \
242  of the loop.
243  :type end: :py:class:`psyclone.psyir.nodes.Node`
244  :param step: the PSyIR node determining the value for the loop \
245  step.
246  :type step: :py:class:`psyclone.psyir.nodes.Node`
247  :param children: a list of PSyIR nodes contained in the \
248  loop.
249  :type children: list of :py:class:`psyclone.psyir.nodes.Node`
250 
251  :returns: a Loop instance.
252  :rtype: :py:class:`psyclone.psyir.nodes.Loop`
253 
254  :raises GenerationError: if the arguments to the create method \
255  are not of the expected type.
256 
257  '''
258  cls._check_variable_check_variable(variable)
259 
260  if not isinstance(children, list):
261  raise GenerationError(
262  f"children argument in create method of Loop class "
263  f"should be a list but found '{type(children).__name__}'.")
264 
265  loop = cls(variable=variable)
266  schedule = Schedule(parent=loop, children=children)
267  loop.children = [start, stop, step, schedule]
268  return loop
269 
270  def _check_completeness(self):
271  ''' Check that the Loop has 4 children and the 4th is a Schedule.
272 
273  :raises InternalError: If the loop does not have 4 children or the
274  4th one is not a Schedule
275  '''
276  # We cannot just do str(self) in this routine we can end up being
277  # called as a result of str(self) higher up the call stack
278  # (because loop bounds are evaluated dynamically).
279  if len(self.childrenchildrenchildren) < 4:
280  raise InternalError(
281  f"Loop is incomplete. It should have exactly 4 "
282  f"children, but found loop with "
283  f"'{', '.join([str(child) for child in self.children])}'.")
284 
285  @property
286  def start_expr(self):
287  '''
288  :returns: the PSyIR Node representing the Loop start expression.
289  :rtype: :py:class:`psyclone.psyir.nodes.Node`
290 
291  '''
292  self._check_completeness_check_completeness()
293  return self._children_children[0]
294 
295  @start_expr.setter
296  def start_expr(self, expr):
297  ''' Setter for Loop start_expr attribute.
298 
299  :param expr: New PSyIR start expression.
300  :type expr: :py:class:`psyclone.psyir.nodes.Node`
301 
302  '''
303  self._check_completeness_check_completeness()
304  self._children_children[0] = expr
305 
306  @property
307  def stop_expr(self):
308  '''
309  :returns: the PSyIR Node representing the Loop stop expression.
310  :rtype: :py:class:`psyclone.psyir.nodes.Node`
311 
312  '''
313  self._check_completeness_check_completeness()
314  return self._children_children[1]
315 
316  @stop_expr.setter
317  def stop_expr(self, expr):
318  ''' Setter for Loop stop_expr attribute.
319 
320  :param expr: New PSyIR stop expression.
321  :type expr: :py:class:`psyclone.psyir.nodes.Node`
322 
323  '''
324  self._check_completeness_check_completeness()
325  self._children_children[1] = expr
326 
327  @property
328  def step_expr(self):
329  '''
330  :returns: the PSyIR Node representing the Loop step expression.
331  :rtype: :py:class:`psyclone.psyir.nodes.Node`
332 
333  '''
334  self._check_completeness_check_completeness()
335  return self._children_children[2]
336 
337  @step_expr.setter
338  def step_expr(self, expr):
339  ''' Setter for Loop step_expr attribute.
340 
341  :param expr: New PSyIR step expression.
342  :type expr: :py:class:`psyclone.psyir.nodes.Node`
343 
344  '''
345  self._check_completeness_check_completeness()
346  self._children_children[2] = expr
347 
348  @property
349  def loop_body(self):
350  '''
351  :returns: the PSyIR Schedule with the loop body statements.
352  :rtype: :py:class:`psyclone.psyir.nodes.Schedule`
353 
354  '''
355  self._check_completeness_check_completeness()
356  return self._children_children[3]
357 
358  @property
359  def dag_name(self):
360  ''' Return the name to use in a dag for this node
361 
362  :returns: Return the dag name for this loop
363  :rtype: string
364 
365  :raises InternalError: if this Loop has no ancestor Routine.
366 
367  '''
368  routine = self.ancestorancestor(Routine)
369  if not routine:
370  raise InternalError(f"Cannot generate DAG name for loop node "
371  f"'{self}' because it is not contained within "
372  f"a Routine.")
373  _, position = self._find_position_find_position(routine)
374 
375  return "loop_" + str(position)
376 
377  def node_str(self, colour=True):
378  '''
379  Returns the name of this node with (optional) control codes
380  to generate coloured output in a terminal that supports it.
381 
382  :param bool colour: whether or not to include colour control codes.
383 
384  :returns: description of this node, possibly coloured.
385  :rtype: str
386 
387  '''
388  result = f"{self.coloured_name(colour)}["
389  result += f"variable='{self.variable.name}'"
390  if self.loop_typeloop_type:
391  result += f", loop_type='{self.loop_type}'"
392  return result + "]"
393 
394  @property
395  def variable(self):
396  '''
397  :returns: the control variable for this loop.
398  :rtype: :py:class:`psyclone.psyir.symbols.DataSymbol`
399  '''
400  self._check_variable_check_variable(self._variable_variable)
401  return self._variable_variable
402 
403  @variable.setter
404  def variable(self, var):
405  '''
406  Setter for the variable associated with this loop.
407 
408  :param var: the control variable reference.
409  :type var: :py:class:`psyclone.psyir.symbols.DataSymbol`
410 
411  '''
412  self._check_variable_check_variable(var)
413  self._variable_variable = var
414 
415  def __str__(self):
416  # Give Loop sub-classes a specialised name
417  name = self.__class__.__name__
418  result = name + "["
419  result += f"variable:'{self.variable.name}'"
420  if self.loop_typeloop_type:
421  result += f", loop_type:'{self.loop_type}'"
422  result += "]\n"
423  for entity in self._children_children:
424  result += str(entity) + "\n"
425  result += "End " + name
426  return result
427 
428  def reference_accesses(self, var_accesses):
429  '''Get all variable access information. It combines the data from
430  the loop bounds (start, stop and step), as well as the loop body.
431  The loop variable is marked as 'READ+WRITE' and references in start,
432  stop and step are marked as 'READ'.
433 
434  :param var_accesses: VariablesAccessInfo instance that stores the \
435  information about variable accesses.
436  :type var_accesses: \
437  :py:class:`psyclone.core.VariablesAccessInfo`
438  '''
439 
440  # Only add the loop variable and start/stop/step values if this is
441  # not an LFRic domain loop. We need to access the variable directly
442  # to avoid a crash in the getter if the loop variable is not defined.
443  if self._variable_variable:
444  # It is important to first add the WRITE access, since this way
445  # the dependency analysis for declaring openmp private variables
446  # will automatically declare the loop variables to be private
447  # (write access before read)
448  var_accesses.add_access(Signature(self.variablevariablevariablevariable.name),
449  AccessType.WRITE, self)
450  var_accesses.add_access(Signature(self.variablevariablevariablevariable.name),
451  AccessType.READ, self)
452 
453  # Accesses of the start/stop/step expressions
454  self.start_exprstart_exprstart_expr.reference_accesses(var_accesses)
455  self.stop_exprstop_exprstop_expr.reference_accesses(var_accesses)
456  self.step_exprstep_exprstep_expr.reference_accesses(var_accesses)
457  var_accesses.next_location()
458 
459  for child in self.loop_bodyloop_body.children:
460  child.reference_accesses(var_accesses)
461  var_accesses.next_location()
462 
464  test_all_variables=False,
465  signatures_to_ignore=None,
466  dep_tools=None):
467  '''This function analyses a loop in the PSyIR to see whether
468  its iterations are independent.
469 
470  :param bool test_all_variables: if True, it will test if all variable
471  accesses are independent, otherwise it will stop after the first
472  variable access is found that isn't.
473  :param signatures_to_ignore: list of signatures for which to skip
474  the access checks.
475  :type signatures_to_ignore: Optional[
476  List[:py:class:`psyclone.core.Signature`]]
477  :param dep_tools: an optional instance of DependencyTools so that the
478  caller can access any diagnostic messages detailing why the loop
479  iterations are not independent.
480  :type dep_tools: Optional[
481  :py:class:`psyclone.psyir.tools.DependencyTools`]
482 
483  :returns: True if the loop iterations are independent, False otherwise.
484  :rtype: bool
485 
486  '''
487  if not dep_tools:
488  # pylint: disable=import-outside-toplevel
489  from psyclone.psyir.tools import DependencyTools
490  dtools = DependencyTools()
491  else:
492  dtools = dep_tools
493  return dtools.can_loop_be_parallelised(
494  self, test_all_variables=test_all_variables,
495  signatures_to_ignore=signatures_to_ignore)
496 
497  def gen_code(self, parent):
498  '''
499  Generate the Fortran Loop and any associated code.
500 
501  :param parent: the node in the f2pygen AST to which to add content.
502  :type parent: :py:class:`psyclone.f2pygen.SubroutineGen`
503 
504  '''
505  # Avoid circular dependency
506  # pylint: disable=import-outside-toplevel
507  from psyclone.psyGen import zero_reduction_variables
508 
509  if not self.is_openmp_parallelis_openmp_parallel():
510  calls = self.reductionsreductions()
511  zero_reduction_variables(calls, parent)
512 
513  # TODO #1010: The Fortran backend operates on a copy of the node so
514  # that the lowering changes are not reflected in the provided node.
515  # This is the correct behaviour but it means that the lowering changes
516  # to ancestors will be lost here because the ancestors use gen_code
517  # instead of lowering+backend.
518  # So we need to do the "rename_and_write" here for the invoke symbol
519  # table to be updated.
520  from psyclone.psyGen import CodedKern
521  for kernel in self.walkwalk(CodedKern):
522  if not kernel.module_inline:
523  if kernel.modified:
524  kernel.rename_and_write()
525 
526  # Use the Fortran Backend from this point
527  parent.add(PSyIRGen(parent, self))
528 
529  # TODO #1010: The Fortran backend operates on a copy of the node so
530  # that the lowering changes are not reflected in the provided node.
531  # This is the correct behaviour but it means that the lowering changes
532  # to ancestors will be lost here because the ancestors use gen_code
533  # instead of lowering+backend.
534  # Therefore we need to replicate the lowering ancestor changes
535  # manually here (all this can be removed when the invoke schedule also
536  # uses the lowering+backend), these are:
537  # - Declaring the loop variable symbols
538  for loop in self.walkwalk(Loop):
539  # pylint: disable=protected-access
540  if loop._variable is None:
541  # This is the dummy iteration variable
542  name = "dummy"
543  kind_gen = None
544  else:
545  name = loop.variable.name
546  kind = loop.variable.datatype.precision.name
547  kind_gen = None if kind == "UNDEFINED" else kind
548  my_decl = DeclGen(parent, datatype="integer",
549  kind=kind_gen,
550  entity_decls=[name])
551  parent.add(my_decl)
552 
553  # - Add the kernel module import statements
554  for kernel in self.walkwalk(CodedKern):
555  if not kernel.module_inline:
556  parent.add(UseGen(parent, name=kernel._module_name, only=True,
557  funcnames=[kernel._name]))
def _check_variable(variable)
Definition: loop.py:185
def create(cls, variable, start, stop, step, children)
Definition: loop.py:229
def start_expr(self, expr)
Definition: loop.py:296
def node_str(self, colour=True)
Definition: loop.py:377
dictionary _loop_type_inference_rules
Definition: loop.py:87
def gen_code(self, parent)
Definition: loop.py:497
def reference_accesses(self, var_accesses)
Definition: loop.py:428
def _check_completeness(self)
Definition: loop.py:270
def variable(self, var)
Definition: loop.py:404
def set_loop_type_inference_rules(cls, rules)
Definition: loop.py:134
def independent_iterations(self, test_all_variables=False, signatures_to_ignore=None, dep_tools=None)
Definition: loop.py:466
def stop_expr(self, expr)
Definition: loop.py:317
def __eq__(self, other)
Definition: loop.py:106
def step_expr(self, expr)
Definition: loop.py:338
def children(self, my_children)
Definition: node.py:935
def reductions(self, reprod=None)
Definition: node.py:1396
def _find_position(self, children, position=None)
Definition: node.py:1015
def walk(self, my_type, stop_type=None, depth=None)
Definition: node.py:1075
def ancestor(self, my_type, excluding=None, include_self=False, limit=None, shared_with=None)
Definition: node.py:1161