# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2017-2026, Science and Technology Facilities Council.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# -----------------------------------------------------------------------------
# Authors R. W. Ford, A. R. Porter, S. Siso and N. Nobre, STFC Daresbury Lab
# I. Kavcic, Met Office
# J. Henrichs, Bureau of Meteorology
# -----------------------------------------------------------------------------
''' This module contains the DataSymbol and its interfaces.'''
from __future__ import annotations
from psyclone.psyir.symbols.typed_symbol import TypedSymbol
from psyclone.psyir.symbols.interfaces import StaticInterface
from psyclone.psyir.symbols.symbol import Symbol
[docs]
class DataSymbol(TypedSymbol):
'''
Symbol identifying a data element. It contains information about:
the datatype, the shape (in column-major order) and the interface
to that symbol (i.e. Local, Global, Argument).
:param str name: name of the symbol.
:param datatype: data type of the symbol.
:type datatype: :py:class:`psyclone.psyir.symbols.DataType`
:param bool is_constant: whether this DataSymbol is a compile-time
constant (default is False). If True then an `initial_value` must
also be provided.
:param initial_value: sets a fixed known expression as an initial
value for this DataSymbol. If `is_constant` is True then this
Symbol will always have this value. If the value is None then this
symbol does not have an initial value (and cannot be a constant).
Otherwise it can receive PSyIR expressions or Python intrinsic types
available in the TYPE_MAP_TO_PYTHON map. By default it is None.
:type initial_value: Optional[item of TYPE_MAP_TO_PYTHON |
:py:class:`psyclone.psyir.nodes.Node`]
:param kwargs: additional keyword arguments provided by
:py:class:`psyclone.psyir.symbols.TypedSymbol`
:type kwargs: unwrapped dict.
'''
def __init__(self, name, datatype, is_constant=False, initial_value=None,
**kwargs):
super().__init__(name, datatype)
self._is_constant = False
self._initial_value = None
self._process_arguments(is_constant=is_constant,
initial_value=initial_value,
**kwargs)
def _process_arguments(self, **kwargs):
''' Process the arguments for the constructor and the specialise
methods. In this case the initial_value and is_constant arguments.
:param kwargs: keyword arguments which can be:\n
:param bool is_constant: whether this DataSymbol is a compile-time
constant (default is False). If True then an `initial_value`
must also be provided.\n
:param initial_value: sets a fixed known expression as an initial
value for this DataSymbol. If `is_constant` is True then this
Symbol will always have this value. If the value is None then
this symbol does not have an initial value (and cannot be a
constant). Otherwise it can receive PSyIR expressions or Python
intrinsic types available in the TYPE_MAP_TO_PYTHON map. By
default it is None.\n
:type initial_value: Optional[item of TYPE_MAP_TO_PYTHON |
:py:class:`psyclone.psyir.nodes.Node`]\n
and the arguments in :py:class:`psyclone.psyir.symbols.TypedSymbol`
:type kwargs: unwrapped dict.
:raises ValueError: if the symbol is a run-time constant but is not
given an initial value.
:raises ValueError: if the symbol is a run-time constant and an
interface other than StaticInterface is specified.
'''
new_initial_value = None
new_is_constant_value = None
# We need to consume 'initial_value' and 'is_constant' before calling
# the super because otherwise there will be an unknown argument in
# kwargs. However, we can only call the 'initial_value' setter after
# the super because it uses self.datatype which in turn is set in
# the super.
if "initial_value" in kwargs:
new_initial_value = kwargs.pop("initial_value")
if not hasattr(self, '_initial_value'):
# Initialise this attribute if we reach this point and this object
# doesn't already have it (which may happen if this symbol has been
# specialised from a Symbol).
self._initial_value = None
if "is_constant" in kwargs:
new_is_constant_value = kwargs.pop("is_constant")
if not hasattr(self, '_is_constant'):
# At least initialise it if we reach this point and it doesn't
# exist (which may happen if this symbol has been specialised from
# a Symbol).
self._is_constant = False
# Record whether an explicit value has been supplied for 'interface'
# (before it is consumed by the super method).
interface_supplied = "interface" in kwargs
super()._process_arguments(**kwargs)
# Now that we have a datatype we can use initial_value setter
# with proper error checking.
if new_initial_value is not None:
self.initial_value = new_initial_value
# Now that we know whether or not we have an initial_value and an
# interface, we can call the is_constant setter.
if new_is_constant_value is not None:
self.is_constant = new_is_constant_value
if self.is_constant and not interface_supplied and self.is_automatic:
# No explicit interface was supplied and this Symbol represents
# a runtime constant so change its interface to be static if it
# would otherwise default to Automatic.
self.interface = StaticInterface()
@property
def is_constant(self):
'''
:returns: Whether the symbol is a compile-time constant (True) or
not (False).
:rtype: bool
'''
return self._is_constant
@is_constant.setter
def is_constant(self, value):
'''
:param bool value: whether or not this symbol is a compile-time
constant.
:raises ValueError: if `value` is True but this symbol does not have an
initial value or an import or unresolved interface.
'''
if (value and not (self.is_import or self.is_unresolved) and
self.initial_value is None):
# A Symbol of UnsupportedType could have initialisation within its
# original declaration.
# pylint: disable=import-outside-toplevel
from psyclone.psyir.symbols.datatypes import UnsupportedType
if not isinstance(self.datatype, UnsupportedType):
raise ValueError(
f"DataSymbol '{self.name}' cannot be a constant because it"
f" does not have an initial value or an import or "
f"unresolved interface.")
self._is_constant = value
@property
def initial_value(self):
'''
:returns: the initial value associated with this symbol (if any).
:rtype: :py:class:`psyclone.psyir.nodes.Node`
'''
return self._initial_value
@initial_value.setter
def initial_value(self, new_value):
'''
:param new_value: set or change the initial value associated
with this DataSymbol. If the value is None then this symbol does
not have an initial value (and cannot be a constant). Otherwise it
can receive PSyIR expressions or Python intrinsic types available
in the TYPE_MAP_TO_PYTHON map.
:type new_value: Optional[item of TYPE_MAP_TO_PYTHON |
:py:class:`psyclone.psyir.nodes.Node`]
:raises ValueError: if a non-None value is provided and 1) this
DataSymbol instance represents an argument, or 2) this
DataSymbol instance is not a scalar (as the shape attribute is
not empty), or 3) an initial value is provided but the type of
the value is not supported, or 4) the type of the value
provided is not compatible with the datatype of this DataSymbol
instance, or 5) the provided PSyIR expression is unsupported.
:raises ValueError: if a None value is provided and this DataSymbol
represents a constant and is not imported.
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import (
Assignment, Node, Literal, Operation, Reference,
CodeBlock, IntrinsicCall)
from psyclone.psyir.symbols.datatypes import (ScalarType, ArrayType,
UnsupportedType)
if new_value is not None:
if self.is_argument:
raise ValueError(
f"Error setting initial value for symbol '{self.name}'. "
f"A DataSymbol with an ArgumentInterface can not have an "
f"initial value.")
if not isinstance(self.datatype,
(ScalarType, ArrayType, UnsupportedType)):
raise ValueError(
f"Error setting initial value for symbol '{self.name}'. "
f"A DataSymbol with an initial value must be a scalar or "
f"an array or of UnsupportedType but found "
f"'{type(self.datatype).__name__}'.")
if isinstance(new_value, Node):
for node in new_value.walk(Node):
if not isinstance(node, (Literal, Operation, Reference,
CodeBlock, IntrinsicCall)):
raise ValueError(
f"Error setting initial value for symbol "
f"'{self.name}'. PSyIR static expressions can only"
f" contain PSyIR Literal, Operation, Reference,"
f" IntrinsicCall or CodeBlock nodes but found: "
f"{node}")
new_initial_value = new_value
else:
# No need to check that self.datatype has an intrinsic
# attribute as we know it is a ScalarType or ArrayType
# due to an earlier test.
lookup = ScalarType.TYPE_MAP_TO_PYTHON[self.datatype.intrinsic]
if not isinstance(new_value, lookup):
raise ValueError(
f"Error setting initial value for symbol "
f"'{self.name}'. This DataSymbol instance datatype is "
f"'{self.datatype}' meaning the initial value should "
f"be '{lookup}' but found '{type(new_value)}'.")
if self.datatype.intrinsic == ScalarType.Intrinsic.BOOLEAN:
# In this case we know new_value is a Python boolean as it
# has passed the isinstance(new_value, lookup) check.
if new_value:
new_initial_value = Literal('true', self.datatype)
else:
new_initial_value = Literal('false', self.datatype)
else:
# Otherwise we convert the Python intrinsic to a PSyIR
# Literal using its string representation.
new_initial_value = Literal(str(new_value), self.datatype)
# Add it to a properly formed Assignment parent, this implicitly
# guarantees that the node is not attached anywhere else (and is
# unexpectedly modified) and also makes it similar to any other RHS
# expression, enabling some functionality without special cases.
# Note that the parent dangles on top of the init value, and is not
# referenced directly from anywhere else.
parent = Assignment()
parent.addchild(Reference(self))
parent.addchild(new_initial_value)
self._initial_value = new_initial_value
else:
if self.is_constant and not self.is_import:
raise ValueError(
f"DataSymbol '{self.name}' is a constant and not imported "
f"and therefore must have an initial value but got None")
self._initial_value = None
def __str__(self):
ret = self.name + ": DataSymbol<" + str(self.datatype)
ret += ", " + str(self._interface)
if self.initial_value is not None:
ret += f", initial_value={self.initial_value}"
if self.is_constant:
ret += ", constant=True"
return ret + ">"
[docs]
def copy(self):
'''Create and return a copy of this object. Any references to the
original will not be affected so the copy will not be referred
to by any other object.
N.B. any other Symbols referenced in either the datatype or the
initial_value properties of the original symbol will remain unchanged
in the copied object. See the `replace_symbols_using` method.
:returns: An object with the same properties as this symbol object.
:rtype: :py:class:`psyclone.psyir.symbols.DataSymbol`
'''
if self.initial_value is not None:
# Ensure any initial-value expression is also copied. Any Symbols
# that are referenced will be the same objects as in the original
# expression.
new_init_value = self.initial_value.copy()
else:
new_init_value = None
if self.is_array:
# Ensure any References in the shape definition of an ArrayType
# are also copied. They will still point to the
# same Symbols as the original.
new_datatype = self.datatype.copy()
else:
new_datatype = self.datatype
copy = DataSymbol(self.name, new_datatype,
visibility=self.visibility,
interface=self.interface.copy(),
is_constant=self.is_constant,
initial_value=new_init_value)
copy.preceding_comment = self.preceding_comment
copy.inline_comment = self.inline_comment
return copy
[docs]
def copy_properties(self,
symbol_in: DataSymbol,
exclude_interface: bool = False):
'''Replace all properties in this object with the properties from
symbol_in, apart from the name (which is immutable) and visibility.
If `exclude_interface` is True, the interface is also not updated.
:param symbol_in: the symbol from which the properties are copied.
:param exclude_interface: whether to copy the interface from the
provided symbol.
:raises TypeError: if the argument is not the expected type.
'''
if not isinstance(symbol_in, DataSymbol):
raise TypeError(f"Argument should be of type 'DataSymbol' but "
f"found '{type(symbol_in).__name__}'.")
super().copy_properties(symbol_in, exclude_interface=exclude_interface)
self._is_constant = symbol_in.is_constant
self._initial_value = symbol_in.initial_value
self.preceding_comment = symbol_in.preceding_comment
self.inline_comment = symbol_in.inline_comment
[docs]
def replace_symbols_using(self, table_or_symbol):
'''
Replace any Symbols referred to by this object with those in the
supplied SymbolTable (or just the supplied Symbol instance) if they
have matching names. If there is no match for a given Symbol then it
is left unchanged.
:param table_or_symbol: the symbol table from which to get replacement
symbols or a single, replacement Symbol.
:type table_or_symbol: :py:class:`psyclone.psyir.symbols.SymbolTable` |
:py:class:`psyclone.psyir.symbols.Symbol`
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.symbols.datatypes import ArrayType
super().replace_symbols_using(table_or_symbol)
# Ensure any Symbols referenced in the initial value are updated.
if self.initial_value:
self.initial_value.replace_symbols_using(table_or_symbol)
# Ensure any Symbols referenced in the shape are updated.
for dim in self.shape:
if isinstance(dim, ArrayType.Extent):
continue
for bnd in [dim.lower, dim.upper]:
if isinstance(bnd, ArrayType.Extent):
continue
bnd.replace_symbols_using(table_or_symbol)
[docs]
def get_all_accessed_symbols(self) -> set[Symbol]:
'''
:returns: a set of all the symbols accessed inside this Symbol.
'''
symbols = super().get_all_accessed_symbols()
if self.initial_value:
symbols.update(self.initial_value.get_all_accessed_symbols())
return symbols
[docs]
def get_bounds(self, idx: int):
'''
:returns: the lower and upper bounds of the specified (0-indexed)
array dimension.
:rtype: Tuple[:py:class:`psyclone.psyir.nodes.DataNode`,
:py:class:`psyclone.psyir.nodes.DataNode`]
:raises IndexError: if the requested array dimension is invalid.
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import IntrinsicCall, Literal, Reference
from psyclone.psyir.symbols.datatypes import (ArrayType, ScalarType,
UnsupportedFortranType)
shape = None
if isinstance(self.datatype, ArrayType):
shape = self.datatype.shape
elif (isinstance(self.datatype, UnsupportedFortranType) and
self.datatype.partial_datatype):
shape = self.datatype.partial_datatype.shape
if shape and idx >= len(shape):
raise IndexError(
f"DataSymbol '{self.name}' has {len(shape)} dimensions but "
f"bounds for (0-indexed) dimension {idx} requested.")
if not shape or shape[idx] in [ArrayType.Extent.DEFERRED,
ArrayType.Extent.ATTRIBUTE]:
# We have no information on the bounds of this dimension.
bounds = []
for intrinsic in [IntrinsicCall.Intrinsic.LBOUND,
IntrinsicCall.Intrinsic.UBOUND]:
bounds.append(IntrinsicCall.create(
intrinsic, [Reference(self),
("dim", Literal(str(idx+1),
ScalarType.integer_type()))]))
return tuple(bounds)
lower = self.shape[idx].lower.copy()
if not isinstance(self.shape[idx].upper, ArrayType.Extent):
return lower, self.shape[idx].upper.copy()
upper = IntrinsicCall.create(
IntrinsicCall.Intrinsic.UBOUND,
[Reference(self), ("dim", Literal(str(idx+1),
ScalarType.integer_type()))])
return lower, upper