Reference Guide  2.5.0
variables_access_info.py
1 # -----------------------------------------------------------------------------
2 # BSD 3-Clause License
3 #
4 # Copyright (c) 2019-2024, Science and Technology Facilities Council.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright notice, this
11 # list of conditions and the following disclaimer.
12 #
13 # * Redistributions in binary form must reproduce the above copyright notice,
14 # this list of conditions and the following disclaimer in the documentation
15 # and/or other materials provided with the distribution.
16 #
17 # * Neither the name of the copyright holder nor the names of its
18 # contributors may be used to endorse or promote products derived from
19 # this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 # POSSIBILITY OF SUCH DAMAGE.
33 # -----------------------------------------------------------------------------
34 # Author J. Henrichs, Bureau of Meteorology
35 # Modified by: S. Siso, STFC Daresbury Laboratory
36 # A. R. Porter, STFC Daresbury Laboratory
37 # A. B. G. Chalk, STFC Daresbury Laboratory
38 # -----------------------------------------------------------------------------
39 
40 '''This module provides management of variable access information.'''
41 
42 
43 from psyclone.core.component_indices import ComponentIndices
44 from psyclone.core.signature import Signature
45 from psyclone.core.single_variable_access_info import SingleVariableAccessInfo
46 from psyclone.errors import InternalError
47 
48 
49 class VariablesAccessInfo(dict):
50  '''This class stores all `SingleVariableAccessInfo` instances for all
51  variables in the corresponding code section. It maintains 'location'
52  information, which is an integer number that is increased for each new
53  statement. It can be used to easily determine if one access is before
54  another.
55 
56  :param nodes: optional, a single PSyIR node or list of nodes from
57  which to initialise this object.
58  :type nodes: Optional[:py:class:`psyclone.psyir.nodes.Node` |
59  List[:py:class:`psyclone.psyir.nodes.Node`]]
60  :param options: a dictionary with options to influence which variable
61  accesses are to be collected.
62  :type options: Dict[str, Any]
63  :param Any options["COLLECT-ARRAY-SHAPE-READS"]: if this option is set
64  to a True value, arrays used as first parameter to the PSyIR query
65  operators lbound, ubound, or size will be reported as 'read'.
66  Otherwise, these accesses will be ignored.
67  :param Any options["USE-ORIGINAL-NAMES"]: if this option is set to a
68  True value, an imported symbol that is renamed (``use mod, a=>b``)
69  will be reported using the original name (``b`` in the example).
70  Otherwise these symbols will be reported using the renamed name
71  (``a``).
72 
73  :raises InternalError: if the optional options parameter is not a
74  dictionary.
75  :raises InternalError: if the nodes parameter either is a list and
76  contains an element that is not a
77  :py:class:`psyclone.psyir.nodes.Node`, of if nodes is not a list and
78  is not of type :py:class:`psyclone.psyir.nodes.Node`
79 
80  '''
81  # List of valid options and their default values. Note that only the
82  # options method checks this, since it is convenient to pass in options
83  # from the DependencyTools that might contain options for these tools.
84  # COLLECT-ARRAY-SHAPE-READS: controls if access to the shape of an array
85  # (e.g. ``ubound(a)`` are reported as read or not at all. Defaults
86  # to True.
87  # USE-ORIGINAL-NAMES: if set this will report the original names of any
88  # symbol that is being renamed (``use mod, renamed_a=>a``). Defaults
89  # to False.
90  _DEFAULT_OPTIONS = {"COLLECT-ARRAY-SHAPE-READS": False,
91  "USE-ORIGINAL-NAMES": False}
92 
93  def __init__(self, nodes=None, options=None):
94  # This dictionary stores the mapping of signatures to the
95  # corresponding SingleVariableAccessInfo instance.
96  dict.__init__(self)
97 
98  self._options_options = VariablesAccessInfo._DEFAULT_OPTIONS.copy()
99  if options:
100  if not isinstance(options, dict):
101  raise InternalError(f"The options argument for "
102  f"VariablesAccessInfo must be a "
103  f"dictionary or None, but got "
104  f"'{type(options).__name__}'.")
105  self._options_options.update(options)
106 
107  # Stores the current location information
108  self._location_location = 0
109  if nodes:
110  # Import here to avoid circular dependency
111  # pylint: disable=import-outside-toplevel
112  from psyclone.psyir.nodes import Node
113  if isinstance(nodes, list):
114  for node in nodes:
115  if not isinstance(node, Node):
116  raise InternalError(f"Error in VariablesAccessInfo. "
117  f"One element in the node list is "
118  f"not a Node, but of type "
119  f"{type(node)}")
120 
121  node.reference_accesses(self)
122  elif isinstance(nodes, Node):
123  nodes.reference_accesses(self)
124  else:
125  arg_type = str(type(nodes))
126  raise InternalError(f"Error in VariablesAccessInfo. "
127  f"Argument must be a single Node in a "
128  f"schedule or a list of Nodes in a "
129  f"schedule but have been passed an "
130  f"object of type: {arg_type}")
131 
132  def __str__(self):
133  '''Gives a shortened visual representation of all variables
134  and their access mode. The output is one of: READ, WRITE, READ+WRITE,
135  or READWRITE for each variable accessed.
136  READ+WRITE is used if the statement (or set of statements)
137  contain individual read and write accesses, e.g. 'a=a+1'. In this
138  case two accesses to `a` will be recorded, but the summary displayed
139  using this function will be 'READ+WRITE'. Same applies if this object
140  stores variable access information about more than one statement, e.g.
141  'a=b; b=1'. There would be two different accesses to 'b' with two
142  different locations, but the string representation would show this as
143  READ+WRITE. If a variable is is passed to a kernel for which no
144  individual variable information is available, and the metadata for
145  this kernel indicates a READWRITE access, this is marked as READWRITE
146  in the string output.'''
147 
148  all_signatures = self.all_signaturesall_signatures
149  output_list = []
150  for signature in all_signatures:
151  mode = ""
152  if self.has_read_writehas_read_write(signature):
153  mode = "READWRITE"
154  else:
155  if self.is_readis_read(signature):
156  if self.is_writtenis_written(signature):
157  mode = "READ+WRITE"
158  else:
159  mode = "READ"
160  elif self.is_writtenis_written(signature):
161  mode = "WRITE"
162  output_list.append(f"{signature}: {mode}")
163  return ", ".join(output_list)
164 
165  def options(self, key=None):
166  '''Returns the value of the options for a specified key,
167  or None if the key is not specified in the options. If no
168  key is specified, the whole option dictionary is returned.
169 
170  :param key: the option to query, or None if all options should
171  be returned.
172  :type key: Optional[str]
173 
174  :returns: the value of the option associated with the provided key
175  or the whole option dictionary if it is not supplied.
176  :rtype: Union[None, Any, dict]
177 
178  :raises InternalError: if an invalid key is specified.
179 
180  '''
181  if key:
182  if key not in VariablesAccessInfo._DEFAULT_OPTIONS:
183  valids = list(VariablesAccessInfo._DEFAULT_OPTIONS.keys())
184  # This makes sure the message always contains the valid
185  # keys in the same order, important for testing.
186  valids.sort()
187  raise InternalError(f"Option key '{key}' is invalid, it "
188  f"must be one of {valids}.")
189  return self._options_options.get(key, None)
190  return self._options_options
191 
192  @property
193  def location(self):
194  '''Returns the current location of this instance, which is
195  the location at which the next accesses will be stored.
196  See the Developers' Guide for more information.
197 
198  :returns: the current location of this object.
199  :rtype: int'''
200  return self._location_location
201 
202  def next_location(self):
203  '''Increases the location number.'''
204  self._location_location = self._location_location + 1
205 
206  def add_access(self, signature, access_type, node, component_indices=None):
207  '''Adds access information for the variable with the given signature.
208  If the `component_indices` parameter is not an instance of
209  `ComponentIndices`, it is used to construct an instance. Therefore it
210  can be None, a list or a list of lists of PSyIR nodes. In the case of
211  a list of lists, this will be used unmodified to construct the
212  ComponentIndices structures. If it is a simple list, it is assumed
213  that it contains the indices used in accessing the last component
214  of the signature. For example, for `a%b` with
215  `component_indices=[i,j]`, it will create `[[], [i,j]` as component
216  indices, indicating that no index is used in the first component `a`.
217  If the access is supposed to be for `a(i)%b(j)`, then the
218  `component_indices` argument must be specified as a list of lists,
219  i.e. `[[i], [j]]`.
220 
221  :param signature: the signature of the variable.
222  :type signature: :py:class:`psyclone.core.Signature`
223  :param access_type: the type of access (READ, WRITE, ...)
224  :type access_type: :py:class:`psyclone.core.access_type.AccessType`
225  :param node: Node in PSyIR in which the access happens.
226  :type node: :py:class:`psyclone.psyir.nodes.Node` instance
227  :param component_indices: index information for the access.
228  :type component_indices: \
229  :py:class:`psyclone.core.component_indices.ComponentIndices`, or \
230  any other type that can be used to construct a ComponentIndices \
231  instance (None, List[:py:class:`psyclone.psyir.nodes.Node`] \
232  or List[List[:py:class:`psyclone.psyir.nodes.Node`]])
233 
234  '''
235  if not isinstance(signature, Signature):
236  raise InternalError(f"Got '{signature}' of type "
237  f"'{type(signature).__name__}' but expected "
238  f"it to be of type psyclone.core.Signature.")
239 
240  # To make it easier for the user, we allow to implicitly create the
241  # component indices instance here:
242  if not isinstance(component_indices, ComponentIndices):
243  # Handle some convenient cases:
244  # 1. Add the right number of [] if component_indices is None:
245  if component_indices is None:
246  component_indices = [[]] * len(signature)
247  elif isinstance(component_indices, list):
248  # 2. If the argument is a simple list (not a list of lists),
249  # assume that the indices are for the last component, and
250  # add enough [] to give the right number of entries in the
251  # list that is used to create the ComponentIndices instance:
252  is_list_of_lists = all(isinstance(indx, list)
253  for indx in component_indices)
254  if not is_list_of_lists:
255  component_indices = [[]] * (len(signature)-1) \
256  + [component_indices]
257 
258  component_indices = ComponentIndices(component_indices)
259 
260  if len(signature) != len(component_indices):
261  raise InternalError(f"Cannot add '{component_indices}' with "
262  f"length {len(component_indices)} as "
263  f"indices for '{signature}' which "
264  f"requires {len(signature)} elements.")
265 
266  if signature in self:
267  self[signature].add_access_with_location(access_type,
268  self._location_location, node,
269  component_indices)
270  else:
271  var_info = SingleVariableAccessInfo(signature)
272  var_info.add_access_with_location(access_type, self._location_location,
273  node, component_indices)
274  self[signature] = var_info
275 
276  @property
277  def all_signatures(self):
278  ''':returns: all signatures contained in this instance, sorted (in \
279  order to make test results reproducible).
280  :rtype: List[:py:class:`psyclone.core.signature`]
281  '''
282  list_of_vars = list(self.keys())
283  list_of_vars.sort()
284  return list_of_vars
285 
286  def merge(self, other_access_info):
287  '''Merges data from a VariablesAccessInfo instance to the
288  information in this instance.
289 
290  :param other_access_info: the other VariablesAccessInfo instance.
291  :type other_access_info: \
292  :py:class:`psyclone.core.VariablesAccessInfo`
293  '''
294 
295  # For each variable add all accesses. After merging the new data,
296  # we need to increase the location so that all further added data
297  # will have a location number that is larger.
298  max_new_location = 0
299  for signature in other_access_info.all_signatures:
300  var_info = other_access_info[signature]
301  for access_info in var_info.all_accesses:
302  # Keep track of how much we need to update the next location
303  # in this object:
304  if access_info.location > max_new_location:
305  max_new_location = access_info.location
306  new_location = access_info.location + self._location_location
307  if signature in self:
308  var_info = self[signature]
309  else:
310  var_info = SingleVariableAccessInfo(signature)
311  self[signature] = var_info
312 
313  var_info.add_access_with_location(access_info.access_type,
314  new_location,
315  access_info.node,
316  access_info.
317  component_indices)
318  # Increase the current location of this instance by the amount of
319  # locations just merged in
320  self._location_location = self._location_location + max_new_location
321 
322  def is_written(self, signature):
323  '''Checks if the specified variable signature is at least
324  written once.
325 
326  :param signature: signature of the variable.
327  :type signature: :py:class:`psyclone.core.Signature`
328 
329  :returns: True if the specified variable is written (at least \
330  once).
331  :rtype: bool
332 
333  :raises: KeyError if the signature name cannot be found.
334 
335  '''
336  var_access_info = self[signature]
337  return var_access_info.is_written()
338 
339  def is_read(self, signature):
340  '''Checks if the specified variable signature is at least read once.
341 
342  :param signature: signature of the variable
343  :type signature: :py:class:`psyclone.core.Signature`
344 
345  :returns: True if the specified variable name is read (at least \
346  once).
347  :rtype: bool
348 
349  :raises: KeyError if the signature cannot be found.'''
350 
351  var_access_info = self[signature]
352  return var_access_info.is_read()
353 
354  def has_read_write(self, signature):
355  '''Checks if the specified variable signature has at least one
356  READWRITE access (which is typically only used in a function call).
357 
358  :param signature: signature of the variable
359  :type signature: :py:class:`psyclone.core.Signature`
360 
361  :returns: True if the specified variable name has (at least one) \
362  READWRITE access.
363  :rtype: bool
364 
365  :raises: KeyError if the signature cannot be found.'''
366 
367  var_access_info = self[signature]
368  return var_access_info.has_read_write()
369 
370 
371 # ---------- Documentation utils -------------------------------------------- #
372 # The list of module members that we wish AutoAPI to generate
373 # documentation for. (See https://psyclone-ref.readthedocs.io)
374 __all__ = ["VariablesAccessInfo"]
def add_access(self, signature, access_type, node, component_indices=None)