Reference Guide  2.5.0
inline_trans.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2022-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: A. R. Porter, R. W. Ford, A. Chalk and S. Siso, STFC Daresbury Lab
35 
36 '''
37 This module contains the InlineTrans transformation.
38 
39 '''
40 from psyclone.errors import InternalError, LazyString
41 from psyclone.psyGen import Transformation
42 from psyclone.psyir.nodes import (
43  ArrayReference, ArrayOfStructuresReference, BinaryOperation, Call,
44  CodeBlock, Container, IntrinsicCall, Node, Range, Routine, Reference,
45  Return, Literal, Assignment, StructureMember, StructureReference)
46 from psyclone.psyir.nodes.array_mixin import ArrayMixin
47 from psyclone.psyir.symbols import (
48  ArgumentInterface, ArrayType, DataSymbol, UnresolvedType, INTEGER_TYPE,
49  RoutineSymbol, StaticInterface, Symbol, SymbolError, UnknownInterface,
50  UnsupportedType, IntrinsicSymbol)
52  Reference2ArrayRangeTrans)
54  TransformationError)
55 
56 
57 _ONE = Literal("1", INTEGER_TYPE)
58 
59 
61  '''
62  This transformation takes a Call (which may have a return value)
63  and replaces it with the body of the target routine. It is used as
64  follows:
65 
66  >>> from psyclone.psyir.backend.fortran import FortranWriter
67  >>> from psyclone.psyir.frontend.fortran import FortranReader
68  >>> from psyclone.psyir.nodes import Call, Routine
69  >>> from psyclone.psyir.transformations import InlineTrans
70  >>> code = """
71  ... module test_mod
72  ... contains
73  ... subroutine run_it()
74  ... integer :: i
75  ... real :: a(10)
76  ... do i=1,10
77  ... a(i) = 1.0
78  ... call sub(a(i))
79  ... end do
80  ... end subroutine run_it
81  ... subroutine sub(x)
82  ... real, intent(inout) :: x
83  ... x = 2.0*x
84  ... end subroutine sub
85  ... end module test_mod"""
86  >>> psyir = FortranReader().psyir_from_source(code)
87  >>> call = psyir.walk(Call)[0]
88  >>> inline_trans = InlineTrans()
89  >>> inline_trans.apply(call)
90  >>> # Uncomment the following line to see a text view of the schedule
91  >>> # print(psyir.walk(Routine)[0].view())
92  >>> print(FortranWriter()(psyir.walk(Routine)[0]))
93  subroutine run_it()
94  integer :: i
95  real, dimension(10) :: a
96  <BLANKLINE>
97  do i = 1, 10, 1
98  a(i) = 1.0
99  a(i) = 2.0 * a(i)
100  enddo
101  <BLANKLINE>
102  end subroutine run_it
103  <BLANKLINE>
104 
105  .. warning::
106  Routines/calls with any of the following characteristics are not
107  supported and will result in a TransformationError:
108 
109  * the routine is not in the same file as the call;
110  * the routine contains an early Return statement;
111  * the routine contains a variable with UnknownInterface;
112  * the routine contains a variable with StaticInterface;
113  * the routine contains an UnsupportedType variable with
114  ArgumentInterface;
115  * the routine has a named argument;
116  * the shape of any array arguments as declared inside the routine does
117  not match the shape of the arrays being passed as arguments;
118  * the routine accesses an un-resolved symbol;
119  * the routine accesses a symbol declared in the Container to which it
120  belongs.
121 
122  Some of these restrictions will be lifted by #924.
123 
124  '''
125  def apply(self, node, options=None):
126  '''
127  Takes the body of the routine that is the target of the supplied
128  call and replaces the call with it.
129 
130  :param node: target PSyIR node.
131  :type node: :py:class:`psyclone.psyir.nodes.Routine`
132  :param options: a dictionary with options for transformations.
133  :type options: Optional[Dict[str, Any]]
134 
135  '''
136  self.validatevalidatevalidate(node, options)
137  # The table associated with the scoping region holding the Call.
138  table = node.scope.symbol_table
139  # Find the routine to be inlined.
140  orig_routine = self._find_routine_find_routine(node)
141 
142  if not orig_routine.children or isinstance(orig_routine.children[0],
143  Return):
144  # Called routine is empty so just remove the call.
145  node.detach()
146  return
147 
148  # Ensure we don't modify the original Routine by working with a
149  # copy of it.
150  routine = orig_routine.copy()
151  routine_table = routine.symbol_table
152 
153  # Construct lists of the nodes that will be inserted and all of the
154  # References that they contain.
155  new_stmts = []
156  refs = []
157  for child in routine.children:
158  new_stmts.append(child.copy())
159  refs.extend(new_stmts[-1].walk(Reference))
160 
161  # Shallow copy the symbols from the routine into the table at the
162  # call site.
163  table.merge(routine_table,
164  symbols_to_skip=self._symbols_to_skip_symbols_to_skip(routine_table))
165 
166  # When constructing new references to replace references to formal
167  # args, we need to know whether any of the actual arguments are array
168  # accesses. If they use 'array notation' (i.e. represent a whole array)
169  # then they won't have index expressions and will have been captured
170  # as a Reference.
171  ref2arraytrans = Reference2ArrayRangeTrans()
172 
173  for child in node.arguments:
174  try:
175  # TODO #1858, this won't yet work for arrays inside structures.
176  ref2arraytrans.apply(child)
177  except (TransformationError, ValueError):
178  pass
179 
180  # Replace any references to formal arguments with copies of the
181  # actual arguments.
182  formal_args = routine_table.argument_list
183  for ref in refs[:]:
184  self._replace_formal_arg_replace_formal_arg(ref, node, formal_args)
185 
186  # Store the Routine level symbol table and node's current scope
187  # so we can merge symbol tables later if required.
188  ancestor_table = node.ancestor(Routine).scope.symbol_table
189  scope = node.scope
190 
191  # Copy the nodes from the Routine into the call site.
192  # TODO #924 - while doing this we should ensure that any References
193  # to common/shared Symbols in the inlined code are updated to point
194  # to the ones at the call site.
195  if isinstance(new_stmts[-1], Return):
196  # If the final statement of the routine is a return then
197  # remove it from the list.
198  del new_stmts[-1]
199 
200  if routine.return_symbol:
201  # This is a function
202  assignment = node.ancestor(Assignment)
203  parent = assignment.parent
204  idx = assignment.position-1
205  for child in new_stmts:
206  idx += 1
207  parent.addchild(child, idx)
208  table = parent.scope.symbol_table
209  # Avoid a potential name clash with the original function
210  table.rename_symbol(
211  routine.return_symbol, table.next_available_name(
212  f"inlined_{routine.return_symbol.name}"))
213  node.replace_with(Reference(routine.return_symbol))
214  else:
215  # This is a call
216  parent = node.parent
217  idx = node.position
218  node.replace_with(new_stmts[0])
219  for child in new_stmts[1:]:
220  idx += 1
221  parent.addchild(child, idx)
222 
223  # If the scope we merged the inlined function's symbol table into
224  # is not a Routine scope then we now merge that symbol table into
225  # the ancestor Routine. This avoids issues like #2424 when
226  # applying ParallelLoopTrans to loops containing inlined calls.
227  if ancestor_table is not scope.symbol_table:
228  ancestor_table.merge(scope.symbol_table)
229  replacement = type(scope.symbol_table)()
230  scope.symbol_table.detach()
231  replacement.attach(scope)
232 
233  def _symbols_to_skip(self, table):
234  '''
235  Constructs a list of those Symbols in the table of the called routine
236  that must be excluded when merging that table with the one at the
237  call site.
238 
239  These are:
240  - those Symbols representing routine arguments;
241  - any RoutineSymbol representing the called routine itself.
242 
243  :param table: the symbol table of the routine to be inlined.
244  :type table: :py:class:`psyclone.psyir.symbols.SymbolTable`
245 
246  :returns: those Symbols that must be skipped when merging the
247  supplied table into the one at the call site.
248  :rtype: list[:py:class:`psyclone.psyir.symbols.Symbol`]
249 
250  '''
251  # We need to exclude formal arguments and any RoutineSymbol
252  # representing the routine itself.
253  symbols_to_skip = table.argument_list[:]
254  try:
255  # We don't want or need the symbol representing that routine.
256  rsym = table.lookup_with_tag("own_routine_symbol")
257  if isinstance(rsym, RoutineSymbol):
258  # We only want to skip RoutineSymbols, not DataSymbols (which
259  # we may have if we have a Fortran function).
260  symbols_to_skip.append(rsym)
261  except KeyError:
262  pass
263  return symbols_to_skip
264 
265  def _replace_formal_arg(self, ref, call_node, formal_args):
266  '''
267  Recursively combines any References to formal arguments in the supplied
268  PSyIR expression with the corresponding Reference (actual argument)
269  from the call site to make a new Reference for use in the inlined code.
270  If the supplied node is not a Reference to a formal argument then it is
271  just returned (after we have recursed to any children).
272 
273  :param ref: the expression to update.
274  :type ref: :py:class:`psyclone.psyir.nodes.Node`
275  :param call_node: the call site.
276  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
277  :param formal_args: the formal arguments of the called routine.
278  :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
279 
280  :returns: the replacement reference.
281  :rtype: :py:class:`psyclone.psyir.nodes.Reference`
282 
283  '''
284  if not isinstance(ref, Reference):
285  # Recurse down in case this is e.g. an Operation or Range.
286  for child in ref.children[:]:
287  self._replace_formal_arg_replace_formal_arg(child, call_node, formal_args)
288  return ref
289 
290  if ref.symbol not in formal_args:
291  # The supplied reference is not to a formal argument.
292  return ref
293 
294  # Lookup the actual argument that corresponds to this formal argument.
295  actual_arg = call_node.arguments[formal_args.index(ref.symbol)]
296 
297  # If the local reference is a simple Reference then we can just
298  # replace it with a copy of the actual argument, e.g.
299  #
300  # call my_sub(my_struc%data(i,j))
301  #
302  # subroutine my_sub(var)
303  # ...
304  # var = 0.0
305  #
306  # pylint: disable=unidiomatic-typecheck
307  if type(ref) is Reference:
308  arg_copy = actual_arg.copy()
309  # If the local reference we are replacing has a parent then we
310  # must ensure the parent's child list is updated. (It may not
311  # have a parent if we are in the process of constructing a brand
312  # new reference.)
313  if ref.parent:
314  ref.replace_with(arg_copy)
315  return arg_copy
316 
317  # Local reference is not simple but the actual argument is, e.g.:
318  #
319  # call my_sub(my_struc)
320  #
321  # subroutine my_sub(var)
322  # ...
323  # var%data(i,j) = 0.0
324  #
325  if type(actual_arg) is Reference:
326  ref.symbol = actual_arg.symbol
327  return ref
328 
329  # Neither the actual or local references are simple, i.e. they
330  # include array accesses and/or structure accesses.
331  new_ref = self._replace_formal_struc_arg_replace_formal_struc_arg(actual_arg, ref, call_node,
332  formal_args)
333  # If the local reference we are replacing has a parent then we must
334  # ensure the parent's child list is updated. (It may not have a parent
335  # if we are in the process of constructing a brand new reference.)
336  if ref.parent:
337  ref.replace_with(new_ref)
338  return new_ref
339 
340  def _create_inlined_idx(self, call_node, formal_args,
341  local_idx, decln_start, actual_start):
342  '''
343  Utility that creates the PSyIR for an inlined array-index access
344  expression. This is not trivial since a formal argument may be
345  declared with bounds that are shifted relative to those of an
346  actual argument.
347 
348  If local_idx is the index of the access in the routine;
349  local_decln_start is the starting index of the dimension as
350  declared in the routine;
351  actual_start is the starting index of the slice at the callsite
352  (whether from the array declaration or a slice);
353 
354  then the index of the inlined access will be::
355 
356  inlined_idx = local_idx - local_decln_start + 1 + actual_start - 1
357  = local_idx - local_decln_start + actual_start
358 
359  :param call_node: the Call that we are inlining.
360  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
361  :param formal_args: the formal arguments of the routine being called.
362  :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
363  :param local_idx: a local array-index expression (i.e. appearing \
364  within the routine being inlined).
365  :type local_idx: :py:class:`psyclone.psyir.nodes.Node`
366  :param decln_start: the lower bound of the corresponding array \
367  dimension, as declared inside the routine being inlined.
368  :type decln_start: :py:class:`psyclone.psyir.nodes.Node`
369  :param actual_start: the lower bound of the corresponding array \
370  dimension, as defined at the call site.
371  :type actual_start: :py:class:`psyclone.psyir.nodes.Node`
372 
373  :returns: PSyIR for the corresponding inlined array index.
374  :rtype: :py:class:`psyclone.psyir.nodes.Node`
375 
376  '''
377  if isinstance(local_idx, Range):
378  lower = self._create_inlined_idx_create_inlined_idx(call_node, formal_args,
379  local_idx.start, decln_start,
380  actual_start)
381  upper = self._create_inlined_idx_create_inlined_idx(call_node, formal_args,
382  local_idx.stop, decln_start,
383  actual_start)
384  step = self._replace_formal_arg_replace_formal_arg(local_idx.step, call_node,
385  formal_args)
386  return Range.create(lower.copy(), upper.copy(), step.copy())
387 
388  uidx = self._replace_formal_arg_replace_formal_arg(local_idx, call_node, formal_args)
389  if decln_start == actual_start:
390  # If the starting indices in the actual and formal arguments are
391  # the same then we don't need to shift the index.
392  return uidx
393 
394  ustart = self._replace_formal_arg_replace_formal_arg(decln_start, call_node, formal_args)
395  start_sub = BinaryOperation.create(BinaryOperation.Operator.SUB,
396  uidx.copy(), ustart.copy())
397  return BinaryOperation.create(BinaryOperation.Operator.ADD,
398  start_sub, actual_start.copy())
399 
400  def _update_actual_indices(self, actual_arg, local_ref,
401  call_node, formal_args):
402  '''
403  Create a new list of indices for the supplied actual argument
404  (ArrayMixin) by replacing any Ranges with the appropriate expressions
405  from the local access in the called routine. If there are no Ranges
406  then the returned list of indices just contains copies of the inputs.
407 
408  :param actual_arg: (part of) the actual argument to the routine.
409  :type actual_arg: :py:class:`psyclone.psyir.nodes.ArrayMixin`
410  :param local_ref: the corresponding Reference in the called routine.
411  :param call_node: the call site.
412  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
413  :param formal_args: the formal arguments of the called routine.
414  :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
415 
416  :returns: new indices for the actual argument.
417  :rtype: List[:py:class:`psyclone.psyir.nodes.Node`]
418 
419  '''
420  if isinstance(local_ref, ArrayMixin):
421  local_indices = [idx.copy() for idx in local_ref.indices]
422  # Get the locally-declared shape of the formal argument in case its
423  # bounds are shifted relative to the caller.
424  if isinstance(local_ref.symbol.datatype, ArrayType):
425  local_decln_shape = local_ref.symbol.datatype.shape
426  else:
427  local_decln_shape = []
428 
429  new_indices = [idx.copy() for idx in actual_arg.indices]
430  local_idx_posn = 0
431  for pos, idx in enumerate(new_indices[:]):
432 
433  if not isinstance(idx, Range):
434  continue
435 
436  # Starting index of slice of actual argument.
437  if actual_arg.is_lower_bound(pos):
438  # Range starts at lower bound of argument so that's what
439  # we store.
440  actual_start = actual_arg.get_lbound_expression(pos)
441  else:
442  actual_start = idx.start
443 
444  local_decln_start = None
445  if local_decln_shape:
446  if isinstance(local_decln_shape[local_idx_posn],
447  ArrayType.ArrayBounds):
448  # The formal argument declaration has a shape.
449  local_shape = local_decln_shape[local_idx_posn]
450  local_decln_start = local_shape.lower
451  elif (local_decln_shape[local_idx_posn] ==
452  ArrayType.Extent.DEFERRED):
453  # The formal argument is declared to be allocatable and
454  # therefore has the same bounds as the actual argument.
455  local_shape = None
456  local_decln_start = actual_start
457  if not local_decln_start:
458  local_shape = None
459  local_decln_start = _ONE
460 
461  if local_ref.is_full_range(local_idx_posn):
462  # If the local Range is for the full extent of the formal
463  # argument then the actual Range is defined by that of the
464  # actual argument and no change is required unless the formal
465  # argument is declared as having a Range with an extent that is
466  # less than that supplied. In general we're not going to know
467  # that so we have to be conservative.
468  if local_shape:
469  new = Range.create(local_shape.lower.copy(),
470  local_shape.upper.copy())
471  new_indices[pos] = self._create_inlined_idx_create_inlined_idx(
472  call_node, formal_args,
473  new, local_decln_start, actual_start)
474  else:
475  # Otherwise, the local index expression replaces the Range.
476  new_indices[pos] = self._create_inlined_idx_create_inlined_idx(
477  call_node, formal_args,
478  local_indices[local_idx_posn],
479  local_decln_start, actual_start)
480  # Each Range corresponds to one dimension of the formal argument.
481  local_idx_posn += 1
482  return new_indices
483 
484  def _replace_formal_struc_arg(self, actual_arg, ref, call_node,
485  formal_args):
486  '''
487  Called by _replace_formal_arg() whenever a formal or actual argument
488  involves an array or structure access that can't be handled with a
489  simple substitution, e.g.
490 
491  .. code-block:: fortran
492 
493  call my_sub(my_struc%grid(:,2,:), 10)
494 
495  subroutine my_sub(grid, ngrids)
496  ...
497  do igrid = 1, ngrids
498  do jgrid = ...
499  do i = 1, 10
500  do j = 1, 10
501  grid(igrid, jgrid)%data(i,j) = 0.0
502 
503  The assignment in the inlined code should become
504 
505  .. code-block:: fortran
506 
507  my_struc%grid(igrid,2,jgrid)%data(i,j) = 0.0
508 
509  This routine therefore recursively combines any References to formal
510  arguments in the supplied Reference (including any array-index
511  expressions) with the corresponding Reference
512  from the call site to make a new Reference for use in the inlined code.
513 
514  :param actual_arg: an actual argument to the routine being inlined.
515  :type actual_arg: :py:class:`psyclone.psyir.nodes.Reference`
516  :param ref: the corresponding reference to a formal argument.
517  :type ref: :py:class:`psyclone.psyir.nodes.Reference`
518  :param call_node: the call site.
519  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
520  :param formal_args: the formal arguments of the called routine.
521  :type formal_args: List[:py:class:`psyclone.psyir.symbols.DataSymbol`]
522 
523  :returns: the replacement reference.
524  :rtype: :py:class:`psyclone.psyir.nodes.Reference`
525 
526  '''
527  # The final stage of this method creates a brand new
528  # [ArrayOf]Structure[s]Reference so we have to collect the indices and
529  # members as we walk down both the actual and local references.
530  local_indices = None
531  members = []
532 
533  # Actual arg could be var, var(:)%index, var(i,j)%grid(:) or
534  # var(j)%data(i) etc. Any Ranges must correspond to dimensions of the
535  # formal argument. The validate() method has already ensured that we
536  # do not have any indirect accesses or non-unit strides.
537 
538  if isinstance(ref, ArrayMixin):
539  local_indices = [idx.copy() for idx in ref.indices]
540 
541  # Since a Range can occur at any level of a Structure access in the
542  # actual argument, we walk down it and check each Member. Any Ranges
543  # are updated according to how that dimension is accessed by the
544  # reference inside the routine.
545  cursor = actual_arg
546  while True:
547  if isinstance(cursor, ArrayMixin):
548  new_indices = self._update_actual_indices_update_actual_indices(
549  cursor, ref, call_node, formal_args)
550  members.append((cursor.name, new_indices))
551  else:
552  members.append(cursor.name)
553 
554  if not isinstance(cursor, (StructureMember, StructureReference)):
555  break
556  cursor = cursor.member
557 
558  if not actual_arg.walk(Range) and local_indices:
559  # There are no Ranges in the actual argument but the local
560  # reference is an array access.
561  # Create updated index expressions for that access.
562  new_indices = []
563  for idx in local_indices:
564  new_indices.append(
565  self._replace_formal_arg_replace_formal_arg(
566  idx.copy(), call_node, formal_args))
567  # Replace the last entry in the `members` list with a new array
568  # access.
569  members[-1] = (cursor.name, new_indices)
570 
571  # We now walk down the *local* access, skipping its head (as that is
572  # replaced by the actual arg). We don't need to worry about updating
573  # index expressions in the actual argument as they are independent of
574  # any array accesses within a structure passed as a formal argument.
575  cursor = ref
576  while isinstance(cursor, (StructureReference, StructureMember)):
577  cursor = cursor.member
578  if isinstance(cursor, ArrayMixin):
579  new_indices = []
580  for idx in cursor.indices:
581  # Update each index expression in case it refers to
582  # formal arguments.
583  new_indices.append(
584  self._replace_formal_arg_replace_formal_arg(
585  idx.copy(), call_node, formal_args))
586  members.append((cursor.name, new_indices))
587  else:
588  members.append(cursor.name)
589 
590  # Finally, construct the new Reference using the information we've
591  # collected from both the actual argument and local access.
592  if len(members) > 1:
593  # We have some form of Structure reference.
594  if isinstance(members[0], tuple):
595  # Root of access is an array access.
596  return ArrayOfStructuresReference.create(actual_arg.symbol,
597  members[0][1],
598  members[1:])
599  return StructureReference.create(actual_arg.symbol, members[1:])
600 
601  # Just an array reference.
602  return ArrayReference.create(actual_arg.symbol, members[0][1])
603 
604  def validate(self, node, options=None):
605  '''
606  Checks that the supplied node is a valid target for inlining.
607 
608  :param node: target PSyIR node.
609  :type node: subclass of :py:class:`psyclone.psyir.nodes.Call`
610  :param options: a dictionary with options for transformations.
611  :type options: Optional[Dict[str, Any]]
612 
613  :raises TransformationError: if the supplied node is not a Call or is \
614  an IntrinsicCall.
615  :raises TransformationError: if the routine has a return value.
616  :raises TransformationError: if the routine body contains a Return \
617  that is not the first or last statement.
618  :raises TransformationError: if the routine body contains a CodeBlock.
619  :raises TransformationError: if the called routine has a named \
620  argument.
621  :raises TransformationError: if any of the variables declared within \
622  the called routine are of UnknownInterface.
623  :raises TransformationError: if any of the variables declared within \
624  the called routine have a StaticInterface.
625  :raises TransformationError: if any of the subroutine arguments is of \
626  UnsupportedType.
627  :raises TransformationError: if a symbol of a given name is imported \
628  from different containers at the call site and within the routine.
629  :raises TransformationError: if the routine accesses an un-resolved \
630  symbol.
631  :raises TransformationError: if the number of arguments in the call \
632  does not match the number of formal arguments of the routine.
633  :raises TransformationError: if a symbol declared in the parent \
634  container is accessed in the target routine.
635  :raises TransformationError: if the shape of an array formal argument \
636  does not match that of the corresponding actual argument.
637 
638  '''
639  super().validate(node, options=options)
640 
641  # The node should be a Call.
642  if not isinstance(node, Call):
643  raise TransformationError(
644  f"The target of the InlineTrans transformation "
645  f"should be a Call but found '{type(node).__name__}'.")
646 
647  if isinstance(node, IntrinsicCall):
648  raise TransformationError(
649  f"Cannot inline an IntrinsicCall ('{node.routine.name}')")
650  name = node.routine.name
651  # Check that we can find the source of the routine being inlined.
652  routine = self._find_routine_find_routine(node)
653 
654  if not routine.children or isinstance(routine.children[0], Return):
655  # An empty routine is fine.
656  return
657 
658  return_stmts = routine.walk(Return)
659  if return_stmts:
660  if len(return_stmts) > 1 or not isinstance(routine.children[-1],
661  Return):
662  # Either there is more than one Return statement or there is
663  # just one but it isn't the last statement of the Routine.
664  raise TransformationError(
665  f"Routine '{name}' contains one or more "
666  f"Return statements and therefore cannot be inlined.")
667 
668  if routine.walk(CodeBlock):
669  raise TransformationError(
670  f"Routine '{name}' contains one or more "
671  f"CodeBlocks and therefore cannot be inlined.")
672 
673  # Support for routines with named arguments is not yet implemented.
674  # TODO #924.
675  for arg in node.argument_names:
676  if arg:
677  raise TransformationError(
678  f"Routine '{routine.name}' cannot be inlined because it "
679  f"has a named argument '{arg}' (TODO #924).")
680 
681  table = node.scope.symbol_table
682  routine_table = routine.symbol_table
683 
684  for sym in routine_table.datasymbols:
685  # We don't inline symbols that have an UnsupportedType and are
686  # arguments since we don't know if a simple assingment if
687  # enough (e.g. pointers)
688  if isinstance(sym.interface, ArgumentInterface):
689  if isinstance(sym.datatype, UnsupportedType):
690  raise TransformationError(
691  f"Routine '{routine.name}' cannot be inlined because "
692  f"it contains a Symbol '{sym.name}' which is an "
693  f"Argument of UnsupportedType: "
694  f"'{sym.datatype.declaration}'")
695  # We don't inline symbols that have an UnknownInterface, as we
696  # don't know how they are brought into this scope.
697  if isinstance(sym.interface, UnknownInterface):
698  raise TransformationError(
699  f"Routine '{routine.name}' cannot be inlined because it "
700  f"contains a Symbol '{sym.name}' with an UnknownInterface:"
701  f" '{sym.datatype.declaration}'")
702  # Check that there are no static variables in the routine (because
703  # we don't know whether the routine is called from other places).
704  if (isinstance(sym.interface, StaticInterface) and
705  not sym.is_constant):
706  raise TransformationError(
707  f"Routine '{routine.name}' cannot be inlined because it "
708  f"has a static (Fortran SAVE) interface for Symbol "
709  f"'{sym.name}'.")
710 
711  # We can't handle a clash between (apparently) different symbols that
712  # share a name but are imported from different containers.
713  try:
714  table.check_for_clashes(
715  routine_table,
716  symbols_to_skip=self._symbols_to_skip_symbols_to_skip(routine_table))
717  except SymbolError as err:
718  raise TransformationError(
719  f"One or more symbols from routine '{routine.name}' cannot be "
720  f"added to the table at the call site.") from err
721 
722  # Check for unresolved symbols or for any accessed from the Container
723  # containing the target routine.
724  # TODO #1792 - kind parameters will not be found by simply doing
725  # `walk(Reference)`. Although SymbolTable has the
726  # `precision_datasymbols` property, this only returns those Symbols
727  # that are used to define the precision of other Symbols in the same
728  # table. If a precision symbol is only used within Statements then we
729  # don't currently capture the fact that it is a precision symbol.
730  ref_or_lits = routine.walk((Reference, Literal))
731  # Check for symbols in any initial-value expressions
732  # (including Fortran parameters) or array dimensions.
733  for sym in routine_table.datasymbols:
734  if sym.initial_value:
735  ref_or_lits.extend(
736  sym.initial_value.walk((Reference, Literal)))
737  if isinstance(sym.datatype, ArrayType):
738  for dim in sym.shape:
739  if isinstance(dim, ArrayType.ArrayBounds):
740  if isinstance(dim.lower, Node):
741  ref_or_lits.extend(dim.lower.walk(Reference,
742  Literal))
743  if isinstance(dim.upper, Node):
744  ref_or_lits.extend(dim.upper.walk(Reference,
745  Literal))
746  # Keep a reference to each Symbol that we check so that we can avoid
747  # repeatedly checking the same Symbol.
748  _symbol_cache = set()
749  for lnode in ref_or_lits:
750  if isinstance(lnode, Literal):
751  if not isinstance(lnode.datatype.precision, DataSymbol):
752  continue
753  sym = lnode.datatype.precision
754  else:
755  sym = lnode.symbol
756  # If we've already seen this Symbol then we can skip it.
757  if sym in _symbol_cache:
758  continue
759  _symbol_cache.add(sym)
760  if isinstance(sym, IntrinsicSymbol):
761  continue
762  # We haven't seen this Symbol before.
763  if sym.is_unresolved:
764  try:
765  routine_table.resolve_imports(symbol_target=sym)
766  except KeyError:
767  # The symbol is not (directly) imported into the symbol
768  # table local to the routine.
769  # pylint: disable=raise-missing-from
770  raise TransformationError(
771  f"Routine '{routine.name}' cannot be inlined "
772  f"because it accesses variable '{sym.name}' and this "
773  f"cannot be found in any of the containers directly "
774  f"imported into its symbol table.")
775  else:
776  if sym.name not in routine_table:
777  raise TransformationError(
778  f"Routine '{routine.name}' cannot be inlined because "
779  f"it accesses variable '{sym.name}' from its "
780  f"parent container.")
781 
782  # Check that the shapes of any formal array arguments are the same as
783  # those at the call site.
784  if len(routine_table.argument_list) != len(node.arguments):
786  lambda: f"Cannot inline '{node.debug_string().strip()}' "
787  f"because the number of arguments supplied to the call "
788  f"({len(node.arguments)}) does not match the number of "
789  f"arguments the routine is declared to have "
790  f"({len(routine_table.argument_list)})."))
791 
792  for formal_arg, actual_arg in zip(routine_table.argument_list,
793  node.arguments):
794  # If the formal argument is an array with non-default bounds then
795  # we also need to know the bounds of that array at the call site.
796  if not isinstance(formal_arg.datatype, ArrayType):
797  # Formal argument is not an array so we don't need to do any
798  # further checks.
799  continue
800 
801  if not isinstance(actual_arg, (Reference, Literal)):
802  # TODO #1799 this really needs the `datatype` method to be
803  # extended to support all nodes. For now we have to abort
804  # if we encounter an argument that is not a scalar (according
805  # to the corresponding formal argument) but is not a
806  # Reference or a Literal as we don't know whether the result
807  # of any general expression is or is not an array.
808  # pylint: disable=cell-var-from-loop
810  lambda: f"The call '{node.debug_string()}' cannot be "
811  f"inlined because actual argument "
812  f"'{actual_arg.debug_string()}' corresponds to a "
813  f"formal argument with array type but is not a "
814  f"Reference or a Literal."))
815 
816  # We have an array argument. We are only able to check that the
817  # argument is not re-shaped in the called routine if we have full
818  # type information on the actual argument.
819  # TODO #924. It would be useful if the `datatype` property was
820  # a method that took an optional 'resolve' argument to indicate
821  # that it should attempt to resolve any UnresolvedTypes.
822  if (isinstance(actual_arg.datatype,
823  (UnresolvedType, UnsupportedType)) or
824  (isinstance(actual_arg.datatype, ArrayType) and
825  isinstance(actual_arg.datatype.intrinsic,
826  (UnresolvedType, UnsupportedType)))):
827  raise TransformationError(
828  f"Routine '{routine.name}' cannot be inlined because "
829  f"the type of the actual argument "
830  f"'{actual_arg.symbol.name}' corresponding to an array"
831  f" formal argument ('{formal_arg.name}') is unknown.")
832 
833  formal_rank = 0
834  actual_rank = 0
835  if isinstance(formal_arg.datatype, ArrayType):
836  formal_rank = len(formal_arg.datatype.shape)
837  if isinstance(actual_arg.datatype, ArrayType):
838  actual_rank = len(actual_arg.datatype.shape)
839  if formal_rank != actual_rank:
840  # It's OK to use the loop variable in the lambda definition
841  # because if we get to this point then we're going to quit
842  # the loop.
843  # pylint: disable=cell-var-from-loop
845  lambda: f"Cannot inline routine '{routine.name}' "
846  f"because it reshapes an argument: actual argument "
847  f"'{actual_arg.debug_string()}' has rank {actual_rank}"
848  f" but the corresponding formal argument, "
849  f"'{formal_arg.name}', has rank {formal_rank}"))
850  if actual_rank:
851  ranges = actual_arg.walk(Range)
852  for rge in ranges:
853  ancestor_ref = rge.ancestor(Reference)
854  if ancestor_ref is not actual_arg:
855  # Have a range in an indirect access.
856  # pylint: disable=cell-var-from-loop
858  lambda: f"Cannot inline routine '{routine.name}' "
859  f"because argument '{actual_arg.debug_string()}' "
860  f"has an array range in an indirect access (TODO "
861  f"#924)."))
862  if rge.step != _ONE:
863  # TODO #1646. We could resolve this problem by making
864  # a new array and copying the necessary values into it.
865  # pylint: disable=cell-var-from-loop
867  lambda: f"Cannot inline routine '{routine.name}' "
868  f"because one of its arguments is an array slice "
869  f"with a non-unit stride: "
870  f"'{actual_arg.debug_string()}' (TODO #1646)"))
871 
872  @staticmethod
873  def _find_routine(call_node):
874  '''Searches for the definition of the routine that is being called by
875  the supplied Call.
876 
877  :param call_node: the Call that is to be inlined.
878  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
879 
880  :returns: the PSyIR for the target routine.
881  :rtype: :py:class:`psyclone.psyir.nodes.Routine`
882 
883  :raises InternalError: if the routine symbol is local but the \
884  routine definition is not found.
885  :raises TransformationError: if the routine definition cannot be found.
886 
887  '''
888  name = call_node.routine.name
889  routine_sym = call_node.routine.symbol
890 
891  if routine_sym.is_modulevar:
892  table = routine_sym.find_symbol_table(call_node)
893  for routine in table.node.walk(Routine):
894  if routine.name.lower() == name.lower():
895  return routine
896  raise InternalError(
897  f"Failed to find the source code of the local routine "
898  f"'{routine_sym.name}'.")
899 
900  if routine_sym.is_unresolved:
901 
902  # First check for any wildcard imports and see if they can
903  # be used to resolve the symbol.
904  wildcard_names = []
905  current_table = call_node.scope.symbol_table
906  while current_table:
907  for container_symbol in current_table.containersymbols:
908  if container_symbol.wildcard_import:
909  wildcard_names.append(container_symbol.name)
910  routine = InlineTrans._find_routine_in_container(
911  call_node, container_symbol)
912  if routine:
913  return routine
914  current_table = current_table.parent_symbol_table()
915 
916  # Next check for any "raw" Routines, i.e. ones that are not
917  # in a Container. Such Routines would exist in the PSyIR
918  # as a child of a FileContainer (if the PSyIR contains a
919  # FileContainer). Note, if the PSyIR does contain a
920  # FileContainer, it will be the root node of the PSyIR.
921  for routine in call_node.root.children:
922  if (isinstance(routine, Routine) and
923  routine.name.lower() == name.lower()):
924  return routine
925  raise TransformationError(
926  f"Failed to find the source code of the unresolved "
927  f"routine '{name}' after trying wildcard imports from "
928  f"{wildcard_names} and all routines that are not in "
929  f"containers.")
930 
931  if routine_sym.is_import:
932  container_symbol = routine_sym.interface.container_symbol
933  routine = InlineTrans._find_routine_in_container(
934  call_node, container_symbol)
935  if routine:
936  return routine
937  raise TransformationError(
938  f"Failed to find the source for routine '{routine_sym.name}' "
939  f"imported from '{container_symbol.name}' and therefore "
940  f"cannot inline it.")
941 
942  raise InternalError(
943  f"Routine Symbol '{routine_sym.name}' is not local, "
944  f"unresolved or imported.")
945 
946  @staticmethod
947  def _find_routine_in_container(call_node, container_symbol):
948  '''Searches for the definition of a routine that is being called by
949  the supplied Call. If present, this routine must exist within a
950  container specified by the supplied container symbol.
951 
952  :param call_node: the Call that is to be inlined.
953  :type call_node: :py:class:`psyclone.psyir.nodes.Call`
954 
955  :param container_symbol: the symbol of the container to search.
956  :type container_symbol: \
957  :py:class:`psyclone.psyir.symbols.ContainerSymbol`
958 
959  :returns: the PSyIR for the target routine, if found.
960  :rtype: Optional[:py:class:`psyclone.psyir.nodes.Routine`]
961 
962  '''
963  # The required Routine will exist within a Container and
964  # that Container could exist in the PSyIR as a child of a
965  # FileContainer (if the PSyIR contains a
966  # FileContainer). If the PSyIR does contain a
967  # FileContainer, it will be the root node of the PSyIR.
968  call_routine_sym = call_node.routine
969  for container in call_node.root.children:
970  if (isinstance(container, Container) and
971  container.name.lower() == container_symbol.name.lower()):
972  for routine in container.children:
973  if (isinstance(routine, Routine) and
974  routine.name.lower() ==
975  call_routine_sym.name.lower()):
976  # Check this routine is public
977  routine_sym = container.symbol_table.lookup(
978  routine.name)
979  if routine_sym.visibility == Symbol.Visibility.PUBLIC:
980  return routine
981  # The Container has been found but it does not contain
982  # the expected Routine or the Routine is not public.
983 
984  # Look in the import that names the routine if there is one.
985  table = container.symbol_table
986  try:
987  routine_sym = table.lookup(call_routine_sym.name)
988  if routine_sym.is_import:
989  child_container_symbol = \
990  routine_sym.interface.container_symbol
991  return (InlineTrans._find_routine_in_container(
992  call_node, child_container_symbol))
993  except KeyError:
994  pass
995 
996  # Look in any wildcard imports.
997  for child_container_symbol in table.containersymbols:
998  if child_container_symbol.wildcard_import:
999  result = InlineTrans._find_routine_in_container(
1000  call_node, child_container_symbol)
1001  if result:
1002  return result
1003  # The required Symbol was not found in the Container.
1004  return None
1005  # The specified Container was not found in the PSyIR.
1006  return None
1007 
1008 
1009 # For AutoAPI auto-documentation generation.
1010 __all__ = ["InlineTrans"]
def validate(self, node, options=None)
Definition: psyGen.py:2783
def _update_actual_indices(self, actual_arg, local_ref, call_node, formal_args)
def _create_inlined_idx(self, call_node, formal_args, local_idx, decln_start, actual_start)
def _replace_formal_struc_arg(self, actual_arg, ref, call_node, formal_args)
def _replace_formal_arg(self, ref, call_node, formal_args)