Reference Guide  2.5.0
nemo_arrayrange2loop_trans.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2020-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 # Author R. W. Ford, STFC Daresbury Lab
35 # Modified A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
36 # Modified A. B. G. Chalk, STFC Daresbury Lab
37 
38 '''Module providing a transformation that given an Assignment node to an
39 ArrayReference in its left-hand-side which has at least one PSyIR Range
40 node (equivalent to an array assignment statement in Fortran), it converts it
41 to the equivalent explicit loop representation using a NemoLoop node.
42 
43 '''
44 
45 from psyclone.errors import LazyString, InternalError
46 from psyclone.nemo import NemoLoop
47 from psyclone.psyGen import Transformation
48 from psyclone.psyir.nodes import Range, Reference, ArrayReference, Call, \
49  Assignment, CodeBlock, ArrayMember, Routine, IntrinsicCall, \
50  StructureReference, StructureMember, Node, Literal
51 from psyclone.psyir.nodes.array_mixin import ArrayMixin
52 from psyclone.psyir.symbols import DataSymbol, INTEGER_TYPE, ScalarType, \
53  UnresolvedType, UnsupportedType, ArrayType, NoType
55  TransformationError
56 
57 
59  '''Transformation that given an assignment with an ArrayReference Range
60  in the LHS (equivalent to an array assignment statement in Fortran), it
61  converts it to an explicit loop doing each of the individual element
62  assignments separately. For example:
63 
64  >>> from psyclone.parse.algorithm import parse
65  >>> from psyclone.psyGen import PSyFactory
66  >>> api = "nemo"
67  >>> filename = "tra_adv.F90" # examples/nemo/code
68  >>> ast, invoke_info = parse(filename, api=api)
69  >>> psy = PSyFactory(api).create(invoke_info)
70  >>> schedule = psy.invokes.invoke_list[0].schedule
71  >>> print(schedule.view())
72  >>>
73  >>> from psyclone.psyir.nodes import Range
74  >>> from psyclone.domain.nemo.transformations import \
75  NemoArrayRange2LoopTrans
76  >>> from psyclone.transformations import TransformationError
77  >>>
78  >>> trans = NemoArrayRange2LoopTrans()
79  >>> for my_range in reversed(schedule.walk(Range)):
80  >>> try:
81  >>> trans.apply(my_range)
82  >>> except TransformationError:
83  >>> pass
84  >>> print(schedule.view())
85 
86  The specified Range node must be the outermost Range (specifying
87  an access to an array index) within an Array Reference and the
88  array reference must be on the left-hand-side of an Assignment
89  node. This is required for correctness and if not satisfied the
90  transformation will raise an exception.
91 
92  By default the transformation will reject character arrays,
93  though this can be overriden by setting the
94  allow_string option to True. Note that PSyclone expresses syntax such
95  as `character(LEN=100)` as UnsupportedFortranType, and this
96  transformation will convert unknown or unsupported types to loops.
97 
98  '''
99  def apply(self, node, options=None):
100  ''' Apply the transformation such that, given an assignment with an
101  ArrayReference Range in the LHS (equivalent to an array assignment
102  statement in Fortran), it converts it to an explicit loop doing each
103  of the individual element assignments separately.
104 
105  The Range node is provided to the apply method of the transformation
106  to indicate which array index should be transformed. This can only
107  be applied to the outermost Range of the ArrayReference.
108 
109  This is currently specific to the 'nemo' API in that it will create
110  NemoLoops.
111 
112  :param node: a Range node.
113  :type node: :py:class:`psyclone.psyir.nodes.Range`
114  :param options: a dictionary with options for \
115  transformations. No options are used in this \
116  transformation. This is an optional argument that defaults \
117  to None.
118  :type options: Optional[Dict[str, Any]]
119  :param bool options["allow_string"]: whether to allow the
120  transformation on a character type array range. Defaults to False.
121 
122  '''
123  self.validatevalidatevalidate(node, options)
124 
125  assignment = node.ancestor(Assignment)
126  parent = assignment.parent
127  # Ensure we always use the routine-level symbol table
128  symbol_table = node.ancestor(Routine).symbol_table
129 
130  # Create a new, unique, iteration variable for the new loop
131  loop_variable_symbol = symbol_table.new_symbol(root_name="idx",
132  symbol_type=DataSymbol,
133  datatype=INTEGER_TYPE)
134 
135  # Replace the loop_idx array dimension with the loop variable.
136  n_ranges = None
137  # Just loop the top-level arrays since we just do 1 substitution per
138  # array construct, even if they have nested arrays in turn.
139  for top_level_ref in assignment.walk(ArrayMixin, stop_type=ArrayMixin):
140  # Then start checking with the inner-most array
141  for array in reversed(top_level_ref.walk(ArrayMixin)):
142  current_n_ranges = len([child for child in array.children
143  if isinstance(child, Range)])
144  if current_n_ranges == 0:
145  continue # This sub-expression already has explicit dims
146  if n_ranges is None:
147  n_ranges = current_n_ranges
148  elif n_ranges != current_n_ranges:
149  raise InternalError(
150  "The number of ranges in the arrays within this "
151  "assignment are not equal. Any such case should have "
152  "been dealt with by the validation method or "
153  "represents invalid PSyIR.")
154 
155  idx = array.get_outer_range_index()
156  array.children[idx] = Reference(loop_variable_symbol)
157  break # If one is found, go to the next top level expression
158 
159  # Replace the assignment with the new explicit loop structure
160  position = assignment.position
161  start, stop, step = node.pop_all_children()
162  loop = NemoLoop.create(loop_variable_symbol, start, stop, step,
163  [assignment.detach()])
164  parent.children.insert(position, loop)
165 
166  def __str__(self):
167  return (
168  "Convert the PSyIR assignment for a specified ArrayReference "
169  "Range into a PSyIR NemoLoop.")
170 
171  @property
172  def name(self):
173  '''
174  :returns: the name of the transformation as a string.
175  :rtype: str
176 
177  '''
178  return type(self).__name__
179 
180  def validate(self, node, options=None):
181  '''Perform various checks to ensure that it is valid to apply the
182  NemoArrayRange2LoopTrans transformation to the supplied PSyIR Node.
183 
184  By default the validation will reject character arrays that PSyclone
185  understand as such, though this can be overriden by setting the
186  allow_string option to True. Note that PSyclone expresses syntax such
187  as `character(LEN=100)` as UnsupportedFortranType, and this
188  transformation will convert unknown or unsupported types to loops.
189 
190  :param node: the node that is being checked.
191  :type node: :py:class:`psyclone.psyir.nodes.Range`
192  :param options: a dictionary with options for \
193  transformations. No options are used in this \
194  transformation. This is an optional argument that defaults \
195  to None.
196  :type options: Optional[Dict[str, Any]]
197  :param bool options["allow_string"]: whether to allow the
198  transformation on a character type array range. Defaults to False.
199 
200  :raises TransformationError: if the node argument is not a \
201  Range, if the Range node is not part of an ArrayReference, \
202  if the Range node is not the outermost Range node of the \
203  ArrayReference or if that ArrayReference does not \
204  constitute the left hand side of an Assignment node.
205  :raises TransformationError: if the node argument has nested array \
206  expressions with Ranges or is an invalid tree with ranges in \
207  multiple locations of a structure of arrays.
208  :raises TransformationError: if the node argument contains a \
209  non-elemental Operation or Call.
210  :raises TransformationError: if node contains a character type
211  child and the allow_strings option is
212  not set.
213 
214  '''
215  # Am I Range node?
216  if not isinstance(node, Range):
217  raise TransformationError(
218  f"Error in NemoArrayRange2LoopTrans transformation. The "
219  f"supplied node argument should be a PSyIR Range, but "
220  f"found '{type(node).__name__}'.")
221  # Am I within an array reference?
222  if not node.parent or not isinstance(node.parent, ArrayMixin):
223  raise TransformationError(
224  f"Error in NemoArrayRange2LoopTrans transformation. The "
225  f"supplied node argument should be within an array access "
226  f"node, but found '{type(node.parent).__name__}'.")
227  # Is the array reference within an assignment?
228  assignment = node.ancestor(Assignment)
229  if not assignment:
230  raise TransformationError(
231  f"Error in NemoArrayRange2LoopTrans transformation. The "
232  f"supplied node argument should be within an Assignment node, "
233  f"but found a '{node}' that is not in an assignment.")
234  # Is the array reference the lhs of the assignment?
235  if node not in assignment.lhs.walk(Node):
236  raise TransformationError(
237  "Error in NemoArrayRange2LoopTrans transformation. The "
238  "supplied node argument should be within an array access "
239  "node that is within the left-hand-side of an Assignment "
240  "node, but it is on the right-hand-side.")
241 
242  # We don't support nested range expressions
243  for range_expr in assignment.walk(Range):
244  ancestor_array = range_expr.parent.ancestor(ArrayMixin)
245  if ancestor_array and any(index.walk(Range) for index
246  in ancestor_array.indices):
248  lambda: f"Error in NemoArrayRange2LoopTrans transformation"
249  f". This transformation does not support array assignments"
250  f" that contain nested Range structures, but found:"
251  f"\n{assignment.debug_string()}"))
252 
253  # Do a single walk to avoid doing a separate one for each type we need
254  nodes_to_check = assignment.walk((CodeBlock, Reference))
255 
256  # Does the rhs of the assignment have any operations/calls that are not
257  # elemental?
258  for cnode in assignment.rhs.walk(Call):
259  nodes_to_check.remove(cnode.routine)
260  if isinstance(cnode, IntrinsicCall):
261  if cnode.intrinsic.is_inquiry:
262  continue
263  name = cnode.intrinsic.name
264  type_txt = "IntrinsicCall"
265  else:
266  name = cnode.routine.name
267  type_txt = "Call"
268  if not cnode.is_elemental:
269  # pylint: disable=cell-var-from-loop
271  lambda: f"Error in NemoArrayRange2LoopTrans "
272  f"transformation. This transformation does not support non"
273  f"-elemental {type_txt}s on the rhs of the associated "
274  f"Assignment node, but found '{name}' in:\n"
275  f"{assignment.debug_string()}'."))
276 
277  # Do not allow to transform expressions with CodeBlocks
278  if any(isinstance(n, CodeBlock) for n in nodes_to_check):
280  lambda: f"Error in NemoArrayRange2LoopTrans transformation. "
281  f"This transformation does not support array assignments that"
282  f" contain a CodeBlock anywhere in the expression, but found:"
283  f"\n{assignment.debug_string()}"))
284 
285  references = [n for n in nodes_to_check if isinstance(n, Reference)]
286  for reference in references:
287  # As special case we always allow references to whole arrays as
288  # part of the LBOUND and UBOUND intrinsics, regardless of the
289  # restrictions below (e.g. is a UnresolvedType reference).
290  if isinstance(reference.parent, IntrinsicCall):
291  intrinsic = reference.parent.intrinsic
292  if intrinsic is IntrinsicCall.Intrinsic.LBOUND:
293  continue
294  if intrinsic is IntrinsicCall.Intrinsic.UBOUND:
295  continue
296 
297  # We allow any references that are part of a structure syntax - we
298  # analyse its child components by continuing the reference list
299  if isinstance(reference, (StructureReference, StructureMember)):
300  continue
301 
302  # We allow any references that have explicit array syntax
303  # because we infer that they are not scalars from the context
304  # where they are found (even if they have UnresolvedType)
305  if isinstance(reference, (ArrayReference, ArrayMember)):
306  continue
307 
308  # However, if it doesn't have array accessors or structure syntax,
309  # we must be sure that it represents a scalar.
310  if not isinstance(reference.symbol, DataSymbol) or \
311  not isinstance(reference.symbol.datatype, ScalarType):
312  raise TransformationError(
313  f"Error in NemoArrayRange2LoopTrans transformation. "
314  f"Variable '{reference.symbol.name}' must be a DataSymbol"
315  f" of ScalarType, but it's a '{reference.symbol}'.")
316 
317  # Is the Range node the outermost Range (as if not, the
318  # transformation would be invalid)?
319  for child in node.parent.indices[node.position+1:]:
320  if isinstance(child, Range):
321  raise TransformationError(
322  "Error in NemoArrayRange2LoopTrans transformation. This "
323  "transformation can only be applied to the outermost "
324  "Range.")
325 
326  if not options:
327  options = {}
328  allow_string_array = options.get("allow_string", False)
329  # If we allow string arrays then we can skip the check.
330  if not allow_string_array:
331  # ArrayMixin datatype lookup can fail if the indices contain a
332  # Call or Intrinsic Call. We catch this exception and continue
333  # for now - TODO #1799
334  for child in assignment.walk((Literal, Reference)):
335  try:
336  # Skip unresolved types
337  if (isinstance(child.datatype,
338  (UnresolvedType, UnsupportedType, NoType))
339  or (isinstance(child.datatype, ArrayType) and
340  isinstance(child.datatype.datatype,
341  (UnresolvedType, UnsupportedType)))):
342  continue
343  if (child.datatype.intrinsic ==
344  ScalarType.Intrinsic.CHARACTER):
345  raise TransformationError(
346  "The NemoArrayRange2LoopTrans transformation "
347  "doesn't allow character arrays by default. This "
348  "can be enabled by passing the allow_string "
349  "option to the transformation."
350  )
351  except NotImplementedError:
352  pass
353 
354 
355 # For automatic document generation
356 __all__ = [
357  'NemoArrayRange2LoopTrans']
def validate(self, node, options=None)
Definition: psyGen.py:2799