Reference Guide  2.5.0
node.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 # Modified J. G. Wallwork, Met Office
39 # -----------------------------------------------------------------------------
40 
41 '''
42 This module contains the abstract Node implementation as well as
43 ChildrenList - a custom implementation of list.
44 
45 '''
46 import copy
47 
48 from psyclone.errors import GenerationError, InternalError
49 from psyclone.psyir.symbols import SymbolError
50 
51 # We use the termcolor module (if available) to enable us to produce
52 # coloured, textual representations of Invoke schedules. If it's not
53 # available then we don't use colour.
54 try:
55  # pylint disable=import-outside-toplevel
56  from termcolor import colored
57 except ImportError:
58  # We don't have the termcolor package available so provide
59  # alternative routine
60  def colored(text, _):
61  '''
62  Returns the supplied text argument unchanged. This is a swap-in
63  replacement for when termcolor.colored is not available.
64 
65  :param str text: text to return.
66  :param _: fake argument, only required to match interface \
67  provided by termcolor.colored.
68 
69  :returns: the supplied text, unchanged.
70  :rtype: str
71  '''
72  return text
73 
74 
75 def _graphviz_digraph_class():
76  '''
77  Wrapper that returns the graphviz Digraph type if graphviz is installed
78  and None otherwise.
79 
80  :returns: the graphviz Digraph type or None.
81  :rtype: :py:class:`graphviz.Digraph` or NoneType.
82 
83  '''
84  try:
85  # pylint: disable=import-outside-toplevel
86  import graphviz as gv
87  return gv.Digraph
88  except ImportError:
89  # TODO #11 add a warning to a log file here
90  # silently return if graphviz bindings are not installed
91  return None
92 
93 
94 class ChildrenList(list):
95  '''
96  Customized list to keep track of the children nodes. It is initialised with
97  a callback function that allows the validation of the inserted children.
98  Since this is a subclass of the standard list, all operations (e.g. append,
99  insert, extend, comparisons, list arithmetic operations) are conserved and
100  make use of the validation. They also trigger an update of all ancestor
101  nodes so that action can be taken in order to keep the tree consistent when
102  necessary (e.g. to update the data-movement clauses on an OpenACC data
103  region).
104 
105  :param node: reference to the node where the list belongs.
106  :type node: :py:class:`psyclone.psyir.nodes.Node`
107  :param validation_function: callback function to the validation method.
108  :type validation_function: \
109  function(int, :py:class:`psyclone.psyir.nodes.Node`)
110  :param str validation_text: textual representation of the valid children.
111 
112  '''
113  def __init__(self, node, validation_function, validation_text):
114  super().__init__()
115  self._node_reference_node_reference = node
116  self._validation_function_validation_function = validation_function
117  self._validation_text_validation_text = validation_text
118 
119  def _validate_item(self, index, item):
120  '''
121  Validates the provided index and item before continuing inserting the
122  item into the list.
123 
124  :param int index: position where the item is inserted into.
125  :param item: object that needs to be validated in the given position.
126  :type item: :py:class:`psyclone.psyir.nodes.Node`
127 
128  :raises GenerationError: if the given index and item are not valid \
129  children for this list.
130  '''
131  if not self._validation_function_validation_function(index, item):
132  errmsg = (f"Item '{item.__class__.__name__}' can't be child "
133  f"{index} of "
134  f"'{self._node_reference.coloured_name(False)}'.")
135 
136  if self._validation_text_validation_text == "<LeafNode>":
137  errmsg = (f"{errmsg} "
138  f"{self._node_reference.coloured_name(False)} is a "
139  f"LeafNode and doesn't accept children.")
140  else:
141  errmsg = (f"{errmsg} The valid format is: "
142  f"'{self._validation_text}'.")
143 
144  raise GenerationError(errmsg)
145 
146  def _check_is_orphan(self, item):
147  '''
148  Checks that the provided item is an orphan (has no parent or the parent
149  was predefined in the constructor but the other end of connection has
150  not finalised until now).
151 
152  :param item: object that needs to be validated.
153  :type item: :py:class:`psyclone.psyir.nodes.Node`
154 
155  :raises GenerationError: if the given item is not an orphan.
156  :raises GenerationError: if the item had been provided with a parent \
157  argument on its constructor and this operation is trying to \
158  make its parent a different node.
159 
160  '''
161  if item.parent and not item.has_constructor_parent:
162  raise GenerationError(
163  f"Item '{item.coloured_name(False)}' can't be added as child "
164  f"of '{self._node_reference.coloured_name(False)}' because "
165  f"it is not an orphan. It already has a "
166  f"'{item.parent.coloured_name(False)}' as a parent.")
167 
168  if item.parent and item.has_constructor_parent:
169  if item.parent is not self._node_reference_node_reference:
170  raise GenerationError(
171  f"'{self._node_reference.coloured_name(False)}' cannot be "
172  f"set as parent of '{item.coloured_name(False)}' because "
173  f"its constructor predefined the parent reference to a "
174  f"different '{item.parent.coloured_name(False)}' node.")
175 
176  def _set_parent_link(self, node):
177  '''
178  Set parent connection of the given node to this ChildrenList's node.
179 
180  :param node: node for which the parent connection need to be updated.
181  :type node: :py:class:`psyclone.psyir.nodes.Node`
182 
183  '''
184  # pylint: disable=protected-access
185  node._parent = self._node_reference_node_reference
186  node._has_constructor_parent = False
187 
188  @staticmethod
189  def _del_parent_link(node):
190  '''
191  Delete parent connection of the given node.
192 
193  :param node: node for which the parent connection need to be updated.
194  :type node: :py:class:`psyclone.psyir.nodes.Node`
195 
196  '''
197  # This is done from here with protected access because it's the parent
198  # which is in charge of maintaining its children connections.
199  # pylint: disable=protected-access
200  node._parent = None
201  node._has_constructor_parent = False
202 
203  def append(self, item):
204  ''' Extends list append method with children node validation.
205 
206  :param item: item to be appened to the list.
207  :type item: :py:class:`psyclone.psyir.nodes.Node`
208 
209  '''
210  self._validate_item_validate_item(len(self), item)
211  self._check_is_orphan_check_is_orphan(item)
212  super().append(item)
213  self._set_parent_link_set_parent_link(item)
214  self._node_reference_node_reference.update_signal()
215 
216  def __setitem__(self, index, item):
217  ''' Extends list __setitem__ method with children node validation.
218 
219  :param int index: position where to insert the item.
220  :param item: item to be inserted to the list.
221  :type item: :py:class:`psyclone.psyir.nodes.Node`
222 
223  '''
224  self._validate_item_validate_item(index, item)
225  self._check_is_orphan_check_is_orphan(item)
226  self._del_parent_link_del_parent_link(self[index])
227  super().__setitem__(index, item)
228  self._set_parent_link_set_parent_link(item)
229  self._node_reference_node_reference.update_signal()
230 
231  def insert(self, index, item):
232  ''' Extends list insert method with children node validation.
233 
234  :param int index: position where to insert the item.
235  :param item: item to be inserted to the list.
236  :type item: :py:class:`psyclone.psyir.nodes.Node`
237 
238  '''
239  positiveindex = index if index >= 0 else len(self) - index
240  self._validate_item_validate_item(positiveindex, item)
241  self._check_is_orphan_check_is_orphan(item)
242  # Check that all displaced items will still in valid positions
243  for position in range(positiveindex, len(self)):
244  self._validate_item_validate_item(position + 1, self[position])
245  super().insert(index, item)
246  self._set_parent_link_set_parent_link(item)
247  self._node_reference_node_reference.update_signal()
248 
249  def extend(self, items):
250  ''' Extends list extend method with children node validation.
251 
252  :param items: list of items to be appened to the list.
253  :type items: list of :py:class:`psyclone.psyir.nodes.Node`
254 
255  '''
256  for index, item in enumerate(items):
257  self._validate_item_validate_item(len(self) + index, item)
258  self._check_is_orphan_check_is_orphan(item)
259  super().extend(items)
260  for item in items:
261  self._set_parent_link_set_parent_link(item)
262  self._node_reference_node_reference.update_signal()
263 
264  # Methods below don't insert elements but have the potential to displace
265  # or change the order of the items in-place.
266  def __delitem__(self, index):
267  ''' Extends list __delitem__ method with children node validation.
268 
269  :param int index: position where to insert the item.
270 
271  '''
272  positiveindex = index if index >= 0 else len(self) - index
273  for position in range(positiveindex + 1, len(self)):
274  self._validate_item_validate_item(position - 1, self[position])
275  self._del_parent_link_del_parent_link(self[index])
276  super().__delitem__(index)
277  self._node_reference_node_reference.update_signal()
278 
279  def remove(self, item):
280  ''' Extends list remove method with children node validation.
281 
282  :param item: item to be deleted the list.
283  :type item: :py:class:`psyclone.psyir.nodes.Node`
284 
285  '''
286  for position in range(self.index(item) + 1, len(self)):
287  self._validate_item_validate_item(position - 1, self[position])
288  self._del_parent_link_del_parent_link(item)
289  super().remove(item)
290  self._node_reference_node_reference.update_signal()
291 
292  def pop(self, index=-1):
293  ''' Extends list pop method with children node validation.
294 
295  :param int index: position of the item that is popped out, if not \
296  given, the last element is popped out.
297 
298  :returns: the last value or the given index value from the list.
299  :rtype: :py:class:`psyclone.psyir.nodes.Node`
300 
301  '''
302  positiveindex = index if index >= 0 else len(self) - index
303  # Check if displaced items after 'positiveindex' will still be valid
304  for position in range(positiveindex + 1, len(self)):
305  self._validate_item_validate_item(position - 1, self[position])
306  self._del_parent_link_del_parent_link(self[index])
307  obj = super().pop(index)
308  self._node_reference_node_reference.update_signal()
309  return obj
310 
311  def reverse(self):
312  ''' Extends list reverse method with children node validation. '''
313  for index, item in enumerate(self):
314  self._validate_item_validate_item(len(self) - index - 1, item)
315  super().reverse()
316  # Reversing the order of e.g. Statements may alter the read/write
317  # properties of any References.
318  self._node_reference_node_reference.update_signal()
319 
320  def clear(self):
321  ''' Wipes the list. '''
322  for item in self:
323  self._del_parent_link_del_parent_link(item)
324  super().clear()
325  # Signal that the tree has changed.
326  self._node_reference_node_reference.update_signal()
327 
328  def sort(self, reverse=False, key=None):
329  '''Override the default sort() implementation as this is not supported
330  for a ChildrenList.
331 
332  :raises NotImplementedError: it makes no sense to sort the Children of
333  a Node.
334  '''
335  raise NotImplementedError("Sorting the Children of a Node is not "
336  "supported.")
337 
338 
339 class Node():
340  '''
341  Base class for a PSyIR node.
342 
343  :param ast: reference into the fparser2 AST corresponding to this node.
344  :type ast: sub-class of :py:class:`fparser.two.Fortran2003.Base`
345  :param children: the PSyIR nodes that are children of this node.
346  :type children: list of :py:class:`psyclone.psyir.nodes.Node`
347  :param parent: that parent of this node in the PSyIR tree.
348  :type parent: :py:class:`psyclone.psyir.nodes.Node`
349  :param annotations: Tags that provide additional information about \
350  the node. The node should still be functionally correct when \
351  ignoring these tags.
352  :type annotations: list of str
353 
354  :raises TypeError: if a parent is supplied that is not an instance of Node.
355  :raises InternalError: if an invalid annotation tag is supplied.
356 
357  '''
358  # pylint: disable=too-many-public-methods
359  # Define two class constants: START_DEPTH and START_POSITION
360  # START_DEPTH is used to calculate depth of all Nodes in the tree
361  # (1 for main Nodes and increasing for their descendants).
362  START_DEPTH = 0
363  # START_POSITION is used to to calculate position of all Nodes in
364  # the tree (absolute or relative to a parent).
365  START_POSITION = 0
366  # The list of valid annotations for this Node. Populated by sub-class.
367  valid_annotations = tuple()
368  # Textual description of the node. (Set up with None since this is
369  # an abstract class, subclasses need to initialize them with strings.)
370  # In python >= 3 this can be better implemented by creating @classmethod
371  # properties for each of them and chain the ABC @abstractmethod annotation.
372  _children_valid_format = None
373  _text_name = None
374  _colour = None
375 
376  def __init__(self, ast=None, children=None, parent=None, annotations=None):
377  if parent and not isinstance(parent, Node):
378  raise TypeError(f"The parent of a Node must also be a Node but "
379  f"got '{type(parent).__name__}'")
380  self._disable_tree_update_disable_tree_update = True
381  # Keep a record of whether a parent node was supplied when constructing
382  # this object. In this case it still won't appear in the parent's
383  # children list. When both ends of the reference are connected this
384  # will become False.
385  self._has_constructor_parent_has_constructor_parent = parent is not None
386  self._parent_parent = parent
387  self._children_children = ChildrenList(self, self._validate_child_validate_child,
388  self._children_valid_format_children_valid_format)
389  if children:
390  self._children_children.extend(children)
391  # Reference into fparser2 AST (if any)
392  self._ast_ast = ast
393  # Ref. to last fparser2 parse tree node associated with this Node.
394  # This is required when adding directives.
395  self._ast_end_ast_end = None
396  # List of tags that provide additional information about this Node.
397  self._annotations_annotations = []
398  if annotations:
399  for annotation in annotations:
400  if annotation in self.valid_annotationsvalid_annotations:
401  self._annotations_annotations.append(annotation)
402  else:
403  raise InternalError(
404  f"{self.__class__.__name__} with unrecognised "
405  f"annotation '{annotation}', valid "
406  f"annotations are: {self.valid_annotations}.")
407  self._disable_tree_update_disable_tree_update = False
408  self.update_signalupdate_signal()
409 
410  def __eq__(self, other):
411  '''
412  Checks whether two nodes are equal. The basic implementation of this
413  checks whether the nodes are the same type, and whether all children
414  of the nodes are equal, and if so then
415  they are considered equal.
416 
417  :param object other: the object to check equality to.
418 
419  :returns: whether other is equal to self.
420  :rtype: bool
421  '''
422  super().__eq__(other)
423  is_eq = type(self) is type(other)
424  is_eq = is_eq and (len(self.childrenchildrenchildren) == len(other.children))
425  for index, child in enumerate(self.childrenchildrenchildren):
426  is_eq = is_eq and child == other.children[index]
427 
428  return is_eq
429 
430  @staticmethod
431  def _validate_child(position, child):
432  '''
433  Decides whether a given child and position are valid for this node.
434  The generic implementation always returns False, this simplifies the
435  specializations as Leaf nodes will have by default the expected
436  behaviour, and non-leaf nodes need to modify this method to its
437  particular constraints anyway. Issue #765 explores if this method
438  can be auto-generated using the _children_valid_format string.
439 
440  :param int position: the position to be validated.
441  :param child: a child to be validated.
442  :type child: :py:class:`psyclone.psyir.nodes.Node`
443 
444  :return: whether the given child and position are valid for this node.
445  :rtype: bool
446 
447  '''
448  # pylint: disable=unused-argument
449  # Position and child argument names are kept for better documentation,
450  # but the generic method always returns False.
451  return False
452 
453  def coloured_name(self, colour=True):
454  '''
455  Returns the display name of this Node, optionally with colour control
456  codes (requires that the termcolor package be installed).
457 
458  :param bool colour: whether or not to include colour control codes \
459  in the result.
460 
461  :returns: the name of this node, optionally with colour control codes.
462  :rtype: str
463  '''
464  if not self._text_name_text_name:
465  name_string = type(self).__name__
466  else:
467  name_string = self._text_name_text_name
468  if colour:
469  if self._colour_colour is None:
470  raise NotImplementedError(
471  f"The _colour attribute is abstract so needs to be given "
472  f"a string value in the concrete class "
473  f"'{type(self).__name__}'.")
474  try:
475  return colored(name_string, self._colour_colour)
476  except KeyError as info:
477  raise InternalError(
478  f"The _colour attribute in class '{type(self).__name__}' "
479  f"has been set to a colour ('{self._colour}') that is not "
480  f"supported by the termcolor package.") from info
481  return name_string
482 
483  def node_str(self, colour=True):
484  '''
485  :param bool colour: whether or not to include control codes for \
486  coloured text.
487 
488  :returns: a text description of this node. Will typically be \
489  overridden by sub-class.
490  :rtype: str
491  '''
492  text = self.coloured_namecoloured_name(colour) + "["
493  if self.annotationsannotations:
494  text += "annotations='" + ','.join(self.annotationsannotations) + "'"
495  text += "]"
496  return text
497 
498  def __str__(self):
499  return self.node_strnode_str(False)
500 
501  @property
502  def ast(self):
503  '''
504  :returns: a reference to that part of the fparser2 parse tree that \
505  this node represents or None.
506  :rtype: sub-class of :py:class:`fparser.two.utils.Base`
507  '''
508  return self._ast_ast
509 
510  @property
511  def ast_end(self):
512  '''
513  :returns: a reference to the last node in the fparser2 parse tree \
514  that represents a child of this PSyIR node or None.
515  :rtype: sub-class of :py:class:`fparser.two.utils.Base`
516  '''
517  return self._ast_end_ast_end
518 
519  @ast.setter
520  def ast(self, ast):
521  '''
522  Set a reference to the fparser2 node associated with this Node.
523 
524  :param ast: fparser2 node associated with this Node.
525  :type ast: :py:class:`fparser.two.utils.Base`
526  '''
527  self._ast_ast = ast
528 
529  @ast_end.setter
530  def ast_end(self, ast_end):
531  '''
532  Set a reference to the last fparser2 node associated with this Node.
533 
534  :param ast: last fparser2 node associated with this Node.
535  :type ast: :py:class:`fparser.two.utils.Base`
536  '''
537  self._ast_end_ast_end = ast_end
538 
539  @property
540  def annotations(self):
541  ''' Return the list of annotations attached to this Node.
542 
543  :returns: List of anotations
544  :rtype: list of str
545  '''
546  return self._annotations_annotations
547 
548  def dag(self, file_name='dag', file_format='svg'):
549  '''
550  Create a dag of this node and its children, write it to file and
551  return the graph object.
552 
553  :param str file_name: name of the file to create.
554  :param str file_format: format of the file to create. (Must be one \
555  recognised by Graphviz.)
556 
557  :returns: the graph object or None (if the graphviz bindings are not \
558  installed).
559  :rtype: :py:class:`graphviz.Digraph` or NoneType
560 
561  :raises GenerationError: if the specified file format is not \
562  recognised by Graphviz.
563 
564  '''
565  digraph = _graphviz_digraph_class()
566  if digraph is None:
567  return None
568  try:
569  graph = digraph(format=file_format)
570  except ValueError as err:
571  raise GenerationError(f"unsupported graphviz file format "
572  f"'{file_format}' provided") from err
573  self.dag_gendag_gen(graph)
574  graph.render(filename=file_name)
575  return graph
576 
577  def dag_gen(self, graph):
578  '''Output my node's graph (dag) information and call any
579  children. Nodes with children are represented as two vertices,
580  a start and an end. Forward dependencies are represented as
581  green edges, backward dependencies are represented as red
582  edges (but their direction is reversed so the layout looks
583  reasonable) and parent child dependencies are represented as
584  blue edges.'''
585  # Import outside top-level to avoid circular dependencies.
586  # pylint: disable=too-many-branches, import-outside-toplevel
587  from psyclone.psyir.nodes.loop import Loop
588  from psyclone.psyir.nodes.routine import Routine
589 
590  # names to append to my default name to create start and end vertices
591  start_postfix = "_start"
592  end_postfix = "_end"
593  if self.childrenchildrenchildren:
594  # I am represented by two vertices, a start and an end
595  graph.node(self.dag_namedag_name+start_postfix)
596  graph.node(self.dag_namedag_name+end_postfix)
597  else:
598  # I am represented by a single vertex
599  graph.node(self.dag_namedag_name)
600  # first deal with forward dependencies
601  remote_node = self.forward_dependenceforward_dependence()
602  local_name = self.dag_namedag_name
603  if self.childrenchildrenchildren:
604  # edge will come from my end vertex as I am a forward dependence
605  local_name += end_postfix
606  if remote_node:
607  # this node has a forward dependence
608  remote_name = remote_node.dag_name
609  if remote_node.children:
610  # the remote node has children so I will connect to
611  # its start vertex
612  remote_name += start_postfix
613  # Create the forward dependence edge in green
614  graph.edge(local_name, remote_name, color="green")
615  elif not isinstance(self, Routine):
616  # If this node is not a Routine (where the DAG context finishes)
617  # and has no forward dependence, connect it to the end vertex
618  # of its parent. Use blue to indicate a parent child
619  # relationship.
620  remote_name = self.parentparent.dag_name + end_postfix
621  graph.edge(local_name, remote_name, color="blue")
622  # now deal with backward dependencies. When creating the edges
623  # we reverse the direction of the dependence (place
624  # remote_node before local_node) to help with the graph
625  # layout
626  remote_node = self.backward_dependencebackward_dependence()
627  local_name = self.dag_namedag_name
628  if self.childrenchildrenchildren:
629  # the edge will come from my start vertex as I am a
630  # backward dependence
631  local_name += start_postfix
632  if remote_node:
633  # this node has a backward dependence.
634  remote_name = remote_node.dag_name
635  if remote_node.children:
636  # the remote node has children so I will connect to
637  # its end vertex
638  remote_name += end_postfix
639  # Create the backward dependence edge in red.
640  graph.edge(remote_name, local_name, color="red")
641  elif not isinstance(self, Routine):
642  # If this node is not a Routine (where the DAG context finishes)
643  # and has no backward dependence, connect it to the start vertex
644  # of its parent. Use blue to indicate a parent child
645  # relationship.
646  remote_name = self.parentparent.dag_name + start_postfix
647  graph.edge(remote_name, local_name, color="blue")
648  # now call any children so they can add their information to
649  # the graph
650  if isinstance(self, Loop):
651  # In case of a loop only look at the body (the other part
652  # of the tree contain start, stop, step values):
653  self.loop_body.dag_gen(graph)
654  else:
655  for child in self.childrenchildrenchildren:
656  child.dag_gen(graph)
657 
658  @property
659  def dag_name(self):
660  '''Return the dag name for this node. This includes the name of the
661  class and the index of its relative position to the parent Routine. If
662  no parent Routine is found, the index used is the absolute position
663  in the tree.
664 
665  :returns: the dag name for this node.
666  :rtype: str
667 
668  '''
669  # Import here to avoid circular dependencies
670  # pylint: disable=import-outside-toplevel
671  from psyclone.psyir.nodes import Routine
672  if self.ancestorancestor(Routine):
673  _, position = self._find_position_find_position(self.ancestorancestor(Routine))
674  else:
675  position = self.abs_positionabs_position
676  return self.coloured_namecoloured_name(False) + "_" + str(position)
677 
678  @property
679  def args(self):
680  '''Return the list of arguments associated with this Node. The default
681  implementation assumes the Node has no directly associated
682  arguments (i.e. is not a Kern class or subclass). Arguments of
683  any of this nodes descendants are considered to be
684  associated. '''
685  args = []
686  for call in self.kernelskernels():
687  args.extend(call.args)
688  return args
689 
691  '''Returns the closest preceding Node that this Node has a direct
692  dependence with or None if there is not one. Only Nodes with
693  the same parent as self are returned. Nodes inherit their
694  descendants' dependencies. The reason for this is that for
695  correctness a node must maintain its parent if it is
696  moved. For example a halo exchange and a kernel call may have
697  a dependence between them but it is the loop body containing
698  the kernel call that the halo exchange must not move beyond
699  i.e. the loop body inherits the dependencies of the routines
700  within it.'''
701  dependence = None
702  # look through all the backward dependencies of my arguments
703  for arg in self.argsargs:
704  dependent_arg = arg.backward_dependence()
705  if dependent_arg:
706  # this argument has a backward dependence
707  node = dependent_arg.call
708  # if the remote node is deeper in the tree than me
709  # then find the ancestor that is at the same level of
710  # the tree as me.
711  while node.depth > self.depthdepth:
712  node = node.parent
713  if self.sameParentsameParent(node) and node is not self:
714  # The remote node (or one of its ancestors) shares
715  # the same parent as me (but its not me)
716  if not dependence:
717  # this is the first dependence found so keep it
718  dependence = node
719  else:
720  # we have already found a dependence
721  if dependence.position < node.position:
722  # the new dependence is closer to me than
723  # the previous dependence so keep it
724  dependence = node
725  return dependence
726 
728  '''Returns the closest following Node that this Node has a direct
729  dependence with or None if there is not one. Only Nodes with
730  the same parent as self are returned. Nodes inherit their
731  descendants' dependencies. The reason for this is that for
732  correctness a node must maintain its parent if it is
733  moved. For example a halo exchange and a kernel call may have
734  a dependence between them but it is the loop body containing
735  the kernel call that the halo exchange must not move beyond
736  i.e. the loop body inherits the dependencies of the routines
737  within it.'''
738  dependence = None
739  # look through all the forward dependencies of my arguments
740  for arg in self.argsargs:
741  dependent_arg = arg.forward_dependence()
742  if dependent_arg:
743  # this argument has a forward dependence
744  node = dependent_arg.call
745  # if the remote node is deeper in the tree than me
746  # then find the ancestor that is at the same level of
747  # the tree as me.
748  while node.depth > self.depthdepth:
749  node = node.parent
750  if self.sameParentsameParent(node) and node is not self:
751  # The remote node (or one of its ancestors) shares
752  # the same parent as me (but its not me)
753  if not dependence:
754  # this is the first dependence found so keep it
755  dependence = node
756  else:
757  if dependence.position > node.position:
758  # the new dependence is closer to me than
759  # the previous dependence so keep it
760  dependence = node
761  return dependence
762 
763  def is_valid_location(self, new_node, position="before"):
764  '''If this Node can be moved to the new_node
765  (where position determines whether it is before of after the
766  new_node) without breaking any data dependencies then return True,
767  otherwise return False.
768 
769  :param new_node: Node to which this node should be moved.
770  :type new_node: :py:class:`psyclone.psyir.nodes.Node`
771  :param str position: either 'before' or 'after'.
772 
773  :raises GenerationError: if new_node is not an\
774  instance of :py:class:`psyclone.psyir.nodes.Node`.
775  :raises GenerationError: if position is not 'before' or 'after'.
776  :raises GenerationError: if self and new_node do not have the same\
777  parent.
778  :raises GenerationError: self and new_node are the same Node.
779 
780  :returns: whether or not the specified location is valid for this node.
781  :rtype: bool
782 
783  '''
784  # First perform correctness checks
785  # 1: check new_node is a Node
786  if not isinstance(new_node, Node):
787  raise GenerationError(
788  f"In the psyir.nodes.Node.is_valid_location() method the "
789  f"supplied argument is not a Node, it is a "
790  f"'{type(new_node).__name__}'.")
791 
792  # 2: check position has a valid value
793  valid_positions = ["before", "after"]
794  if position not in valid_positions:
795  raise GenerationError(
796  f"The position argument in the psyGenNode.is_valid_location() "
797  f"method must be one of {valid_positions} but found "
798  f"'{position}'")
799 
800  # 3: check self and new_node have the same parent
801  if not self.sameParentsameParent(new_node):
802  raise GenerationError(
803  "In the psyir.nodes.Node.is_valid_location() method "
804  "the node and the location do not have the same parent")
805 
806  # 4: check proposed new position is not the same as current position
807  new_position = new_node.position
808  if new_position < self.positionpositionposition and position == "after":
809  new_position += 1
810  elif new_position > self.positionpositionposition and position == "before":
811  new_position -= 1
812 
813  if self.positionpositionposition == new_position:
814  raise GenerationError(
815  "In the psyir.nodes.Node.is_valid_location() method, the "
816  "node and the location are the same so this transformation "
817  "would have no effect.")
818 
819  # Now determine whether the new location is valid in terms of
820  # data dependencies
821  # Treat forward and backward dependencies separately
822  if new_position < self.positionpositionposition:
823  # the new_node is before this node in the schedule
824  prev_dep_node = self.backward_dependencebackward_dependence()
825  if not prev_dep_node:
826  # There are no backward dependencies so the move is valid
827  return True
828  # return (is the dependent node before the new_position?)
829  return prev_dep_node.position < new_position
830  # new_node.position > self.position
831  # the new_node is after this node in the schedule
832  next_dep_node = self.forward_dependenceforward_dependence()
833  if not next_dep_node:
834  # There are no forward dependencies so the move is valid
835  return True
836  # return (is the dependent node after the new_position?)
837  return next_dep_node.position > new_position
838 
839  @property
840  def depth(self):
841  '''
842  Returns this Node's depth in the tree: 1 for the Schedule
843  and increasing for its descendants at each level.
844  :returns: depth of the Node in the tree
845  :rtype: int
846  '''
847  my_depth = self.START_DEPTHSTART_DEPTH
848  node = self
849  while node is not None:
850  node = node.parent
851  my_depth += 1
852  return my_depth
853 
854  def view(self, depth=0, colour=True, indent=" ", _index=None):
855  '''Output a human readable description of the current node and all of
856  its descendents as a string.
857 
858  :param int depth: depth of the tree hierarchy for output \
859  text. Defaults to 0.
860  :param bool colour: whether to include colour coding in the \
861  output. Defaults to True.
862  :param str indent: the indent to apply as the depth \
863  increases. Defaults to 4 spaces.
864  :param int _index: the position of this node wrt its siblings \
865  or None. Defaults to None.
866 
867  :returns: a representation of this node and its descendents.
868  :rtype: str
869 
870  :raises TypeError: if one of the arguments is the wrong type.
871  :raises ValueError: if the depth argument is negative.
872 
873  '''
874  # Avoid circular import
875  # pylint: disable=import-outside-toplevel
876  from psyclone.psyir.nodes import Schedule
877 
878  if not isinstance(depth, int):
879  raise TypeError(
880  f"depth argument should be an int but found "
881  f"{type(depth).__name__}.")
882  if depth < 0:
883  raise ValueError(
884  f"depth argument should be a positive integer but "
885  f"found {depth}.")
886  if not isinstance(colour, bool):
887  raise TypeError(
888  f"colour argument should be a bool but found "
889  f"{type(colour).__name__}.")
890  if not isinstance(indent, str):
891  raise TypeError(
892  f"indent argument should be a str but found "
893  f"{type(indent).__name__}.")
894 
895  full_indent = depth*indent
896  description = self.node_strnode_str(colour=colour)
897  if not isinstance(self.parentparent, Schedule) or _index is None:
898  result = f"{full_indent}{description}\n"
899  else:
900  result = f"{full_indent}{_index}: {description}\n"
901  children_result_list = []
902  for idx, node in enumerate(self._children_children):
903  children_result_list.append(
904  node.view(
905  depth=depth + 1, _index=idx, colour=colour, indent=indent))
906  result = result + "".join(children_result_list)
907  return result
908 
909  def addchild(self, child, index=None):
910  '''
911  Adds the supplied node as a child of this node (at position index if
912  supplied). The supplied node must not have an existing parent.
913 
914  :param child: the node to add as a child of this one.
915  :type child: :py:class:`psyclone.psyir.nodes.Node`
916  :param index: optional position at which to insert new child. Default \
917  is to append new child to the list of existing children.
918  :type index: Optional[int]
919 
920  '''
921  if index is not None:
922  self._children_children.insert(index, child)
923  else:
924  self._children_children.append(child)
925 
926  @property
927  def children(self):
928  '''
929  :returns: the immediate children of this Node.
930  :rtype: List[:py:class:`psyclone.psyir.nodes.Node`]
931  '''
932  return self._children_children
933 
934  @children.setter
935  def children(self, my_children):
936  ''' Set a new children list.
937 
938  :param my_children: new list of children.
939  :type my_children: list
940 
941  :raises TypeError: if the given children parameter is not a list.
942  '''
943  if isinstance(my_children, list):
944  self.pop_all_childrenpop_all_children() # First remove existing children if any
945  self._children_children = ChildrenList(self, self._validate_child_validate_child,
946  self._children_valid_format_children_valid_format)
947  self._children_children.extend(my_children)
948  else:
949  raise TypeError("The 'my_children' parameter of the node.children"
950  " setter must be a list.")
951 
952  @property
953  def parent(self):
954  '''
955  :returns: the parent node.
956  :rtype: :py:class:`psyclone.psyir.nodes.Node` or NoneType
957  '''
958  return self._parent_parent
959 
960  @property
961  def siblings(self):
962  '''
963  :returns: list of sibling nodes, including self.
964  :rtype: List[:py:class:`psyclone.psyir.nodes.Node`]
965  '''
966  parent = self.parentparent
967  return [self] if parent is None else parent.children
968 
969  @property
971  '''
972  :returns: whether the constructor has predefined a parent connection
973  but the parent's children list doesn't include this node yet.
974  :rtype: bool
975  '''
976  return self._has_constructor_parent_has_constructor_parent
977 
978  @property
979  def position(self):
980  '''
981  Find a Node's position relative to its parent Node (starting
982  with 0 if it does not have a parent).
983 
984  :returns: relative position of a Node to its parent
985  :rtype: int
986  '''
987  if self.parentparent is None:
988  return self.START_POSITIONSTART_POSITION
989  for index, child in enumerate(self.parentparent.children):
990  if child is self:
991  return index
992 
993  @property
994  def abs_position(self):
995  '''
996  Find a Node's absolute position in the tree (starting with 0 if
997  it is the root). Needs to be computed dynamically from the
998  starting position (0) as its position may change.
999 
1000  :returns: absolute position of a Node in the tree.
1001  :rtype: int
1002 
1003  :raises InternalError: if the absolute position cannot be found.
1004 
1005  '''
1006  if self.rootroot is self:
1007  return self.START_POSITIONSTART_POSITION
1008  found, position = self._find_position_find_position(self.rootroot.children,
1009  self.START_POSITIONSTART_POSITION)
1010  if not found:
1011  raise InternalError("Error in search for Node position "
1012  "in the tree")
1013  return position
1014 
1015  def _find_position(self, children, position=None):
1016  '''
1017  Recurse through the tree depth first returning position of
1018  a Node if found.
1019 
1020  :param children: list of Nodes which are children of this Node.
1021  :type children: list of :py:class:`psyclone.psyir.nodes.Node`
1022  :param int position: start counting from this position. Defaults to \
1023  START_POSITION.
1024 
1025  :returns: if the self has been found in the provided children list \
1026  and its relative position.
1027  :rtype: bool, int
1028 
1029  :raises InternalError: if the starting position is < 0.
1030 
1031  '''
1032  if position is None:
1033  position = self.START_POSITIONSTART_POSITION
1034  elif position < self.START_POSITIONSTART_POSITION:
1035  raise InternalError(
1036  f"Search for Node position started from {position} "
1037  f"instead of {self.START_POSITION}.")
1038  for child in children:
1039  position += 1
1040  if child is self:
1041  return True, position
1042  if child.children:
1043  found, position = self._find_position_find_position(child.children, position)
1044  if found:
1045  return True, position
1046  return False, position
1047 
1048  @property
1049  def root(self):
1050  '''
1051  :returns: the root node of the PSyIR tree.
1052  :rtype: :py:class:`psyclone.psyir.nodes.Node`
1053 
1054  '''
1055  # Starting with 'self.parent' instead of 'node = self' avoids many
1056  # false positive pylint issues that assume self.root type would be
1057  # the same as self type.
1058  if self.parentparent is None:
1059  return self
1060  node = self.parentparent
1061  while node.parent is not None:
1062  node = node.parent
1063  return node
1064 
1065  def sameParent(self, node_2):
1066  '''
1067  :returns: True if `node_2` has the same parent as this node, False \
1068  otherwise.
1069  :rtype: bool
1070  '''
1071  if self.parentparent is None or node_2.parent is None:
1072  return False
1073  return self.parentparent is node_2.parent
1074 
1075  def walk(self, my_type, stop_type=None, depth=None):
1076  ''' Recurse through the PSyIR tree and return all objects that are
1077  an instance of 'my_type', which is either a single class or a tuple
1078  of classes. In the latter case all nodes are returned that are
1079  instances of any classes in the tuple. The recursion into the tree
1080  is stopped if an instance of 'stop_type' (which is either a single
1081  class or a tuple of classes) is found. This can be used to avoid
1082  analysing e.g. inlined kernels, or as performance optimisation to
1083  reduce the number of recursive calls. The recursion into the tree is
1084  also stopped if the (optional) 'depth' level is reached.
1085 
1086  :param my_type: the class(es) for which the instances are collected.
1087  :type my_type: type | Tuple[type, ...]
1088  :param stop_type: class(es) at which recursion is halted (optional).
1089  :type stop_type: Optional[type | Tuple[type, ...]]
1090  :param depth: the depth value the instances must have (optional).
1091  :type depth: Optional[int]
1092 
1093  :returns: list with all nodes that are instances of my_type \
1094  starting at and including this node.
1095  :rtype: List[:py:class:`psyclone.psyir.nodes.Node`]
1096 
1097  '''
1098  local_list = []
1099  if isinstance(self, my_type) and depth in [None, self.depthdepth]:
1100  local_list.append(self)
1101 
1102  # Stop recursion further into the tree if an instance of a class
1103  # listed in stop_type is found.
1104  if stop_type and isinstance(self, stop_type):
1105  return local_list
1106 
1107  # Stop recursion further into the tree if a depth level has been
1108  # specified and it is reached.
1109  if depth is not None and self.depthdepth >= depth:
1110  return local_list
1111 
1112  for child in self.childrenchildrenchildren:
1113  local_list += child.walk(my_type, stop_type, depth=depth)
1114  return local_list
1115 
1116  def get_sibling_lists(self, my_type, stop_type=None):
1117  '''
1118  Recurse through the PSyIR tree and return lists of Nodes that are
1119  instances of 'my_type' and are immediate siblings. Here 'my_type' is
1120  either a single class or a tuple of classes. In the latter case all
1121  nodes are returned that are instances of any classes in the tuple. The
1122  recursion into the tree is stopped if an instance of 'stop_type' (which
1123  is either a single class or a tuple of classes) is found.
1124 
1125  :param my_type: the class(es) for which the instances are collected.
1126  :type my_type: type | Tuple[type, ...]
1127  :param stop_type: class(es) at which recursion is halted (optional).
1128  :type stop_type: Optional[type | Tuple[type, ...]]
1129 
1130  :returns: list of lists, each of which containing nodes that are
1131  instances of my_type and are immediate siblings, starting at
1132  and including this node.
1133  :rtype: List[List[:py:class:`psyclone.psyir.nodes.Node`]]
1134 
1135  '''
1136  # Separate nodes by depth
1137  by_depth = {}
1138  for node in self.walkwalk(my_type, stop_type=stop_type):
1139  depth = node.depth
1140  if depth not in by_depth:
1141  by_depth[depth] = []
1142  by_depth[depth].append(node)
1143 
1144  # Determine lists of consecutive nodes
1145  global_list = []
1146  for depth, local_list in sorted(by_depth.items()):
1147  block = []
1148  for node in local_list:
1149  if not block or node.immediately_follows(block[-1]):
1150  block.append(node)
1151  else:
1152  # Current node does not immediately follow the previous one
1153  # so this marks the end of the current block.
1154  global_list.append(block)
1155  block = [node]
1156  if block:
1157  global_list.append(block)
1158  return global_list
1159 
1160  def ancestor(self, my_type, excluding=None, include_self=False,
1161  limit=None, shared_with=None):
1162  '''
1163  Search back up the tree and check whether this node has an ancestor
1164  that is an instance of the supplied type. If it does then we return
1165  it otherwise we return None. An individual (or tuple of) (sub-)
1166  class(es) to ignore may be provided via the `excluding` argument. If
1167  `include_self` is True then the current node is included in the search.
1168  If `limit` is provided then the search ceases if/when the supplied
1169  node is encountered.
1170  If `shared_with` is provided, then the ancestor search will find an
1171  ancestor of both this node and the node provided as `shared_with` if
1172  such an ancestor exists.
1173 
1174  :param my_type: class(es) to search for.
1175  :type my_type: type | Tuple[type, ...]
1176  :param excluding: (sub-)class(es) to ignore or None.
1177  :type excluding: Optional[type | Tuple[type, ...]]
1178  :param bool include_self: whether or not to include this node in the \
1179  search.
1180  :param limit: an optional node at which to stop the search.
1181  :type limit: Optional[:py:class:`psyclone.psyir.nodes.Node`]
1182  :param shared_with: an optional node which must also have the
1183  found node as an ancestor.
1184  :type shared_with: Optional[:py:class:`psyclone.psyir.nodes.Node`]
1185 
1186  :returns: First ancestor Node that is an instance of any of the \
1187  requested classes or None if not found.
1188  :rtype: Optional[:py:class:`psyclone.psyir.nodes.Node`]
1189 
1190  :raises TypeError: if `excluding` is provided but is not a type or \
1191  tuple of types.
1192  :raises TypeError: if `limit` is provided but is not an instance \
1193  of Node.
1194  '''
1195  if include_self:
1196  myparent = self
1197  else:
1198  myparent = self.parentparent
1199 
1200  if excluding is not None:
1201  if isinstance(excluding, type):
1202  excludes = (excluding, )
1203  elif isinstance(excluding, tuple):
1204  excludes = excluding
1205  else:
1206  raise TypeError(
1207  f"The 'excluding' argument to ancestor() must be a type or"
1208  f" a tuple of types but got: '{type(excluding).__name__}'")
1209 
1210  if limit and not isinstance(limit, Node):
1211  raise TypeError(
1212  f"The 'limit' argument to ancestor() must be an instance of "
1213  f"Node but got '{type(limit).__name__}'")
1214 
1215  # If we need to find a shared ancestor, then find a starting ancestor
1216  # for the sharing node.
1217  shared_ancestor = None
1218  if shared_with is not None:
1219  shared_ancestor = shared_with.ancestor(
1220  my_type, excluding=excluding,
1221  include_self=include_self, limit=limit)
1222 
1223  while myparent is not None:
1224  if isinstance(myparent, my_type):
1225  if not (excluding and isinstance(myparent, excludes)):
1226  # If this is a valid instance but not the same as for
1227  # the shared_with node, we do logic afterwards to continue
1228  # searching upwards, as we could be either higher or
1229  # lower than the shared_ancestor found previously.
1230  if shared_ancestor and shared_ancestor is not myparent:
1231  break
1232  # This parent node is not an instance of an excluded
1233  # sub-class so return it
1234  return myparent
1235  if myparent is limit:
1236  break
1237  myparent = myparent.parent
1238 
1239  # We search up the tree until we find an ancestor of the requested
1240  # type(s) shared by the shared_with node.
1241  while (myparent is not shared_ancestor and myparent and
1242  shared_ancestor):
1243  if myparent is limit:
1244  break
1245  if myparent.depth > shared_ancestor.depth:
1246  # If myparent is deeper in the tree than the current
1247  # potential shared ancestor, search upwards to find
1248  # the next valid ancestor of this node.
1249  myparent = myparent.ancestor(
1250  my_type, excluding=excluding,
1251  include_self=False, limit=limit)
1252  else:
1253  # shared_ancestor is equal or deeper in the tree than
1254  # myparent, so search upwards for the next valid ancestor
1255  # of shared_ancestor.
1256  shared_ancestor = shared_ancestor.ancestor(
1257  my_type, excluding=excluding, include_self=False,
1258  limit=limit)
1259  # If myparent is shared ancestor then return myparent.
1260  if myparent is shared_ancestor:
1261  return myparent
1262 
1263  # Otherwise we didn't find an ancestor that was valid.
1264  return None
1265 
1266  def kernels(self):
1267  '''
1268  :returns: all kernels that are descendants of this node in the PSyIR.
1269  :rtype: List[:py:class:`psyclone.psyGen.Kern`]
1270  '''
1271  # pylint: disable=import-outside-toplevel
1272  from psyclone.psyGen import Kern
1273  return self.walkwalk(Kern)
1274 
1275  def following(self, routine=True):
1276  '''Return all :py:class:`psyclone.psyir.nodes.Node` nodes after this
1277  node. Ordering is depth first. If the `routine` argument is
1278  set to `True` then nodes are only returned if they are
1279  descendents of this node's closest ancestor routine if one
1280  exists.
1281 
1282  :param bool routine: an optional (default `True`) argument \
1283  that only returns nodes that are within this node's \
1284  closest ancestor Routine node if one exists.
1285 
1286  :returns: a list of nodes.
1287  :rtype: :func:`list` of :py:class:`psyclone.psyir.nodes.Node`
1288 
1289  '''
1290  root = self.rootroot
1291  if routine:
1292  # Import here to avoid circular dependencies
1293  # pylint: disable=import-outside-toplevel
1294  from psyclone.psyir.nodes import Routine
1295  # If there is an ancestor Routine node then only return nodes
1296  # that are within it.
1297  routine_node = self.ancestorancestor(Routine)
1298  if routine_node:
1299  root = routine_node
1300  all_nodes = root.walk(Node)
1301  position = None
1302  for index, node in enumerate(all_nodes):
1303  if node is self:
1304  position = index
1305  break
1306 
1307  return all_nodes[position+1:]
1308 
1309  def preceding(self, reverse=False, routine=True):
1310  '''Return all :py:class:`psyclone.psyir.nodes.Node` nodes before this
1311  node. Ordering is depth first. If the `reverse` argument is
1312  set to `True` then the node ordering is reversed
1313  i.e. returning the nodes closest to this node first. if the
1314  `routine` argument is set to `True` then nodes are only
1315  returned if they are descendents of this node's closest
1316  ancestor routine if one exists.
1317 
1318  :param bool reverse: an optional (default `False`) argument \
1319  that reverses the order of any returned nodes (i.e. makes \
1320  them 'closest first' if set to true.
1321  :param bool routine: an optional (default `True`) argument \
1322  that only returns nodes that are within this node's \
1323  closest ancestor Routine node if one exists.
1324 
1325  :returns: a list of nodes.
1326  :rtype: :func:`list` of :py:class:`psyclone.psyir.nodes.Node`
1327 
1328  '''
1329  root = self.rootroot
1330  if routine:
1331  # Import here to avoid circular dependencies
1332  # pylint: disable=import-outside-toplevel
1333  from psyclone.psyir.nodes import Routine
1334  # If there is an ancestor Routine node then only return nodes
1335  # that are within it.
1336  routine_node = self.ancestorancestor(Routine)
1337  if routine_node:
1338  root = routine_node
1339  all_nodes = root.walk(Node)
1340  position = None
1341  for index, node in enumerate(all_nodes):
1342  if node is self:
1343  position = index
1344  break
1345  nodes = all_nodes[:position]
1346  if reverse:
1347  nodes.reverse()
1348  return nodes
1349 
1350  def immediately_precedes(self, node_2):
1351  '''
1352  :returns: True if this node immediately precedes `node_2`, False
1353  otherwise
1354  :rtype: bool
1355  '''
1356  return (
1357  self.sameParentsameParent(node_2)
1358  and self in node_2.preceding()
1359  and self.positionpositionposition + 1 == node_2.position
1360  )
1361 
1362  def immediately_follows(self, node_1):
1363  '''
1364  :returns: True if this node immediately follows `node_1`, False
1365  otherwise
1366  :rtype: bool
1367  '''
1368  return (
1369  self.sameParentsameParent(node_1)
1370  and self in node_1.following()
1371  and self.positionpositionposition == node_1.position + 1
1372  )
1373 
1374  def coded_kernels(self):
1375  '''
1376  Returns a list of all of the user-supplied kernels (as opposed to
1377  builtins) that are beneath this node in the PSyIR.
1378 
1379  :returns: all user-supplied kernel calls below this node.
1380  :rtype: List[:py:class:`psyclone.psyGen.CodedKern`]
1381 
1382  '''
1383  # pylint: disable=import-outside-toplevel
1384  from psyclone.psyGen import CodedKern
1385  return self.walkwalk(CodedKern)
1386 
1387  def loops(self):
1388  '''
1389  :returns: all loops currently in this schedule.
1390  :rtype: List[:py:class:`psyclone.psyir.nodes.Loop`]
1391  '''
1392  # pylint: disable=import-outside-toplevel
1393  from psyclone.psyir.nodes import Loop
1394  return self.walkwalk(Loop)
1395 
1396  def reductions(self, reprod=None):
1397  '''
1398  Return all kernels that have reductions and are decendents of this
1399  node. If reprod is not provided, all reductions are
1400  returned. If reprod is False, all builtin reductions that are
1401  not set to reproducible are returned. If reprod is True, all
1402  builtins that are set to reproducible are returned.
1403 
1404  :param reprod: if provided, filter reductions by whether or not they \
1405  are set to be reproducible.
1406  :type reprod: Optional[bool]
1407 
1408  :returns: all kernels involving reductions that are descendants of \
1409  this node.
1410  :rtype: List[:py:class:`psyclone.psyir.nodes.Kern`]
1411 
1412  '''
1413  # pylint: disable=import-outside-toplevel
1414  from psyclone.psyGen import Kern
1415 
1416  call_reduction_list = []
1417  for call in self.walkwalk(Kern):
1418  if call.is_reduction:
1419  if reprod is None:
1420  call_reduction_list.append(call)
1421  elif reprod:
1422  if call.reprod_reduction:
1423  call_reduction_list.append(call)
1424  else:
1425  if not call.reprod_reduction:
1426  call_reduction_list.append(call)
1427  return call_reduction_list
1428 
1430  '''
1431  :returns: True if this Node is within an OpenMP parallel region, \
1432  False otherwise.
1433  :rtype: bool
1434 
1435  '''
1436  # pylint: disable=import-outside-toplevel
1437  from psyclone.psyir.nodes import OMPParallelDirective
1438  omp_dir = self.ancestorancestor(OMPParallelDirective)
1439  if omp_dir:
1440  return True
1441  return False
1442 
1444  '''
1445  In-place replacement of high-level concepts into generic language
1446  PSyIR constructs. This generic implementation only recurses down
1447  to its children, but this method must be re-implemented by Nodes
1448  that represent high-level concepts.
1449 
1450  :returns: the lowered version of this node.
1451  :rtype: :py:class:`psyclone.psyir.node.Node`
1452 
1453  '''
1454  # We recurse only over the original children (hence [:]), this is
1455  # because new nodes may be inserted during the lowering, but these
1456  # must already be language-level.
1457  for child in self.childrenchildrenchildren[:]:
1458  child.lower_to_language_level()
1459  return self
1460 
1461  def reference_accesses(self, var_accesses):
1462  '''Get all variable access information. The default implementation
1463  just recurses down to all children.
1464 
1465  :param var_accesses: Stores the output results.
1466  :type var_accesses: \
1467  :py:class:`psyclone.core.VariablesAccessInfo`
1468  '''
1469  for child in self._children_children:
1470  child.reference_accesses(var_accesses)
1471 
1472  @property
1473  def scope(self):
1474  ''' Some nodes (e.g. Schedule and Container) allow symbols to be
1475  scoped via an attached symbol table. This property returns the closest
1476  ScopingNode node including self.
1477 
1478  :returns: the closest ancestor ScopingNode node.
1479  :rtype: :py:class:`psyclone.psyir.node.ScopingNode`
1480 
1481  :raises SymbolError: if there is no ScopingNode ancestor.
1482 
1483  '''
1484  # These imports have to be local to this method to avoid circular
1485  # dependencies.
1486  # pylint: disable=import-outside-toplevel
1487  from psyclone.psyir.nodes.scoping_node import ScopingNode
1488  node = self.ancestorancestor(ScopingNode, include_self=True)
1489  if node:
1490  return node
1491  raise SymbolError(
1492  f"Unable to find the scope of node '{self}' as none of its "
1493  f"ancestors are Container or Schedule nodes.")
1494 
1495  def replace_with(self, node, keep_name_in_context=True):
1496  '''Removes self, and its descendants, from the PSyIR tree to which it
1497  is connected, and replaces it with the supplied node (and its
1498  descendants).
1499 
1500  :param node: the node that will replace self in the PSyIR \
1501  tree.
1502  :type node: :py:class:`psyclone.psyir.nodes.node`
1503  :param bool keep_name_in_context: whether to conserve the name \
1504  referencing this node.
1505 
1506  :raises TypeError: if the argument 'node' is not a Node.
1507  :raises TypeError: if the argument 'keep_name_in_context' is not bool.
1508  :raises GenerationError: if this node does not have a parent.
1509  :raises GenerationError: if the argument 'node' has a parent.
1510 
1511  '''
1512  if not isinstance(node, Node):
1513  raise TypeError(
1514  f"The argument node in method replace_with in the Node class "
1515  f"should be a Node but found '{type(node).__name__}'.")
1516  if not isinstance(keep_name_in_context, bool):
1517  raise TypeError(
1518  f"The argument keep_name_in_context in method replace_with in "
1519  f"the Node class should be a bool but found "
1520  f"'{type(keep_name_in_context).__name__}'.")
1521  if not self.parentparent:
1522  raise GenerationError(
1523  "This node should have a parent if its replace_with method "
1524  "is called.")
1525  if node.parent is not None:
1526  raise GenerationError(
1527  f"The parent of argument node in method replace_with in the "
1528  f"Node class should be None but found "
1529  f"'{type(node.parent).__name__}'.")
1530 
1531  # The n'th argument is placed at the n'th+1 children position
1532  # because the 1st child is the routine reference
1533  if keep_name_in_context and hasattr(self.parentparent, "argument_names") \
1534  and self.parentparent.argument_names[self.positionpositionposition - 1] is not None:
1535  # If it is a named context it will have a specific method for
1536  # replacing the node while keeping the name
1537  name = self.parentparent.argument_names[self.positionpositionposition - 1]
1538  self.parentparent.replace_named_arg(name, node)
1539  else:
1540  self.parentparent.children[self.positionpositionposition] = node
1541 
1542  def pop_all_children(self):
1543  ''' Remove all children of this node and return them as a list.
1544 
1545  :returns: all the children of this node as orphan nodes.
1546  :rtype: list of :py:class:`psyclone.psyir.node.Node`
1547 
1548  '''
1549  free_children = []
1550  while self.childrenchildrenchildren:
1551  free_children.insert(0, self.childrenchildrenchildren.pop())
1552  return free_children
1553 
1554  def detach(self):
1555  ''' Detach this node from the tree it belongs to. This is necessary
1556  as a precursor to inserting it as the child of another node.
1557 
1558  :returns: this node detached from its parent.
1559  :rtype: :py:class:`psyclone.psyir.node.Node`
1560 
1561  '''
1562  if self.parentparent:
1563  index = self.positionpositionposition
1564  self.parentparent.children.pop(index)
1565  return self
1566 
1567  def _refine_copy(self, other):
1568  ''' Refine the object attributes when a shallow copy is not the most
1569  appropriate operation during a call to the copy() method.
1570 
1571  :param other: object we are copying from.
1572  :type other: :py:class:`psyclone.psyir.node.Node`
1573 
1574  '''
1575  # Disable tree-updating during this operation (since it is a copy we
1576  # know we don't need to change the tree structure).
1577  self._disable_tree_update_disable_tree_update = True
1578  self._parent_parent = None
1579  self._has_constructor_parent_has_constructor_parent = False
1580  self._annotations_annotations = other.annotations[:]
1581  # Invalidate shallow copied children list
1582  self._children_children = ChildrenList(self, self._validate_child_validate_child,
1583  self._children_valid_format_children_valid_format)
1584  # And make a recursive copy of each child instead
1585  self.childrenchildrenchildren.extend([child.copy() for child in other.children])
1586  self._disable_tree_update_disable_tree_update = False
1587 
1588  def copy(self):
1589  ''' Return a copy of this node. This is a bespoke implementation for
1590  PSyIR nodes that will deepcopy some of its recursive data-structure
1591  (e.g. the children tree), while not copying other attributes (e.g.
1592  top-level parent reference).
1593 
1594  :returns: a copy of this node and its children.
1595  :rtype: :py:class:`psyclone.psyir.node.Node`
1596 
1597  '''
1598  # Start with a shallow copy of the object
1599  new_instance = copy.copy(self)
1600  # and then refine the elements that shouldn't be shallow copied
1601  # pylint: disable=protected-access
1602  new_instance._refine_copy(self)
1603  return new_instance
1604 
1606  ''' Validates this Node in the context of the whole PSyIR tree.
1607  Although there are validation checks for the parent<->child
1608  relationships, there are other constraints that can only be
1609  checked once the tree is complete and all transformations have
1610  been applied. (One example is that an OMP Do directive must be
1611  within the scope of an OMP Parallel directive.)
1612 
1613  By default, this routine does nothing. It must be overridden
1614  appropriately in any sub-classes to which constraints apply.
1615  If an error is found then a GenerationError should be raised.
1616 
1617  '''
1618 
1619  def debug_string(self):
1620  ''' Generates a Fortran-like output representation but without
1621  lowering high-level nodes. This is fast to generate because it
1622  doesn't deepcopy the tree like the Language backends and its
1623  output, although not compilable, is readable for error messages.
1624 
1625  :returns: a Fortran-like output representation of the tree.
1626  :rtype: str
1627 
1628  '''
1629  # Import outside top-level to avoid circular dependencies.
1630  # pylint: disable=import-outside-toplevel
1631  from psyclone.psyir.backend.debug_writer import DebugWriter
1632  return DebugWriter()(self)
1633 
1634  def origin_string(self):
1635  ''' Generates a string with the available information about where
1636  this node has been created. It currently only works with Fortran
1637  Statements or subchildren of them.
1638 
1639  :returns: a string specifing the origin of this node.
1640  :rtype: str
1641  '''
1642  name = self.coloured_namecoloured_name(False)
1643  line_span = "<unknown>"
1644  original_src = "<unknown>"
1645  filename = "<unknown>"
1646  # Try to populate the line/src/filename using the ancestor Statement
1647  from psyclone.psyir.nodes.statement import Statement
1648  node = self.ancestorancestor(Statement, include_self=True)
1649  # TODO #2062: The part below is tighly coupled to fparser tree
1650  # structure and ideally should be moved the appropriate frontend,
1651  # but we don't necessarely want to do all this string manipulation
1652  # ahead of time as it is rarely needed. One option is for the frontend
1653  # to provide a callable that this method will invoke to generate the
1654  # string. Other frontends or PSyIR not comming from code could provide
1655  # a completely different implementation in the Callable oject.
1656  if node and node._ast:
1657  if hasattr(node._ast, 'item') and node._ast.item:
1658  if hasattr(node._ast.item, 'reader'):
1659  if hasattr(node._ast.item.reader, 'file'):
1660  filename = node._ast.item.reader.file.name
1661  line_span = node._ast.item.span
1662  original_src = node._ast.item.line
1663  return (f"{name} from line {line_span} of file "
1664  f"'{filename}':\n> {original_src}")
1665 
1666  def update_signal(self):
1667  '''
1668  Called whenever there is a change in the PSyIR tree below this node.
1669  It is responsible for ensuring that this method does not get called
1670  recursively and then calls the _update_node() method of the current
1671  node (which is the only part that subclasses should specialise).
1672  Finally, it propagates the update signal up to the parent node
1673  (if any).
1674 
1675  '''
1676  # Ensure that update_signal does not get called recursively.
1677  if self._disable_tree_update_disable_tree_update:
1678  return
1679 
1680  # Perform the update, disabling the recursive call of this routine on
1681  # this node.
1682  self._disable_tree_update_disable_tree_update = True
1683  self._update_node_update_node()
1684  self._disable_tree_update_disable_tree_update = False
1685 
1686  # Propagate the signal up the tree.
1687  if self._parent_parent:
1688  self._parent_parent.update_signal()
1689 
1690  def _update_node(self):
1691  '''
1692  Specify how this node must be updated when an update_signal is
1693  received. The modifications in this method will not trigger a
1694  recursive signal (i.e. they won't cause this node to attempt to
1695  update itself again).
1696 
1697  This base implementation does nothing.
1698  '''
1699 
1700  def path_from(self, ancestor):
1701  ''' Find the path in the psyir tree between ancestor and node and
1702  returns a list containing the path.
1703 
1704  The result of this method can be used to find the node from its
1705  ancestor for example by:
1706 
1707  >>> index_list = node.path_from(ancestor)
1708  >>> cursor = ancestor
1709  >>> for index in index_list:
1710  >>> cursor = cursor.children[index]
1711  >>> assert cursor is node
1712 
1713  :param ancestor: an ancestor node of self to find the path from.
1714  :type ancestor: :py:class:`psyclone.psyir.nodes.Node`
1715 
1716  :raises ValueError: if ancestor is not an ancestor of self.
1717 
1718  :returns: a list of child indices representing the path between
1719  ancestor and self.
1720  :rtype: List[int]
1721  '''
1722  result_list = []
1723  current_node = self
1724  while current_node is not ancestor and current_node.parent is not None:
1725  result_list.append(current_node.position)
1726  current_node = current_node.parent
1727 
1728  if current_node is not ancestor:
1729  raise ValueError(f"Attempted to find path_from a non-ancestor "
1730  f"'{type(ancestor).__name__}' node.")
1731 
1732  result_list.reverse()
1733  return result_list
1734 
1735 
1736 # For automatic documentation generation
1737 # TODO #913 the 'colored' routine shouldn't be in this module.
1738 __all__ = ["colored",
1739  "ChildrenList",
1740  "Node"]
def _set_parent_link(self, node)
Definition: node.py:176
def insert(self, index, item)
Definition: node.py:231
def _validate_item(self, index, item)
Definition: node.py:119
def sort(self, reverse=False, key=None)
Definition: node.py:328
def __setitem__(self, index, item)
Definition: node.py:216
def _check_is_orphan(self, item)
Definition: node.py:146
def children(self, my_children)
Definition: node.py:935
def __eq__(self, other)
Definition: node.py:410
def backward_dependence(self)
Definition: node.py:690
def sameParent(self, node_2)
Definition: node.py:1065
def dag_gen(self, graph)
Definition: node.py:577
def preceding(self, reverse=False, routine=True)
Definition: node.py:1309
def is_valid_location(self, new_node, position="before")
Definition: node.py:763
def dag(self, file_name='dag', file_format='svg')
Definition: node.py:548
def addchild(self, child, index=None)
Definition: node.py:909
def lower_to_language_level(self)
Definition: node.py:1443
def reductions(self, reprod=None)
Definition: node.py:1396
def forward_dependence(self)
Definition: node.py:727
def immediately_follows(self, node_1)
Definition: node.py:1362
def immediately_precedes(self, node_2)
Definition: node.py:1350
def get_sibling_lists(self, my_type, stop_type=None)
Definition: node.py:1116
def _validate_child(position, child)
Definition: node.py:431
def has_constructor_parent(self)
Definition: node.py:970
def replace_with(self, node, keep_name_in_context=True)
Definition: node.py:1495
def node_str(self, colour=True)
Definition: node.py:483
def coloured_name(self, colour=True)
Definition: node.py:453
def _find_position(self, children, position=None)
Definition: node.py:1015
def path_from(self, ancestor)
Definition: node.py:1700
def reference_accesses(self, var_accesses)
Definition: node.py:1461
def following(self, routine=True)
Definition: node.py:1275
def view(self, depth=0, colour=True, indent=" ", _index=None)
Definition: node.py:854
def walk(self, my_type, stop_type=None, depth=None)
Definition: node.py:1075
def validate_global_constraints(self)
Definition: node.py:1605
def ancestor(self, my_type, excluding=None, include_self=False, limit=None, shared_with=None)
Definition: node.py:1161