# -----------------------------------------------------------------------------
# BSD 3-Clause License
#
# Copyright (c) 2019-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
# Modified J. Henrichs, Bureau of Meteorology
# Modified A. B. G. Chalk, STFC Daresbury Lab
# -----------------------------------------------------------------------------
''' This module contains the datatype definitions.'''
from __future__ import annotations
import abc
import copy
from collections import OrderedDict
from dataclasses import dataclass
from enum import Enum
from typing import Any, Optional, Union, TYPE_CHECKING
from psyclone.configuration import Config
from psyclone.errors import InternalError
from psyclone.psyir.commentable_mixin import CommentableMixin
from psyclone.psyir.symbols.datasymbol import DataSymbol
from psyclone.psyir.symbols.data_type_symbol import DataTypeSymbol
from psyclone.psyir.symbols.symbol import Symbol
if TYPE_CHECKING:
from psyclone.psyir.nodes.datanode import DataNode
from psyclone.psyir.symbols import SymbolTable
[docs]
class DataType(metaclass=abc.ABCMeta):
'''Abstract base class from which all types are derived.'''
@abc.abstractmethod
def __str__(self):
'''
:returns: a description of this type.
:rtype: str
'''
def __eq__(self, other):
'''
:param Any other: the object to check equality to.
:returns: whether this type is equal to the 'other' type.
:rtype: bool
'''
return type(other) is type(self)
[docs]
def copy(self):
'''
:returns: a copy of this datatype.
:rtype: :py:class:`psyclone.psyir.symbols.datatypes.DataType`
'''
return copy.copy(self)
[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`
'''
[docs]
def get_all_accessed_symbols(self) -> set[Symbol]:
'''
:returns: a set of all the symbols accessed inside this DataType.
'''
return set()
@property
def is_allocatable(self) -> Optional[bool]:
'''
:returns: whether this DataType is allocatable. In the base class
set this to be always False.'''
return False
[docs]
class UnresolvedType(DataType):
# pylint: disable=too-few-public-methods
''' Indicates that the type declaration has not been found yet. '''
def __str__(self):
return "UnresolvedType"
@property
def is_allocatable(self) -> Optional[bool]:
'''
:returns: whether this DataType is allocatable. In case of an
UnresolvedType we don't know.'''
return None
[docs]
class NoType(DataType):
# pylint: disable=too-few-public-methods
''' Indicates that the associated symbol has an empty type (equivalent
to `void` in C). '''
def __str__(self):
return "NoType"
[docs]
class UnsupportedType(DataType, metaclass=abc.ABCMeta):
'''
Indicates that a variable declaration is not supported by the PSyIR.
This class is abstract and must be subclassed for each language
supported by the PSyIR frontends.
:param str declaration_txt: the original textual declaration of
the symbol.
:raises TypeError: if the supplied declaration_txt is not a str.
'''
def __init__(self, declaration_txt):
if not isinstance(declaration_txt, str):
raise TypeError(
f"UnsupportedType constructor expects the original variable "
f"declaration as a string but got an argument of type "
f"'{type(declaration_txt).__name__}'")
self._declaration = declaration_txt
@abc.abstractmethod
def __str__(self):
''' Abstract method that must be implemented in subclass. '''
# Note, an UnsupportedType is immutable so a declaration setter is not
# allowed. This is to allow subclasses to extract and provide
# parts of the declaration without worrying about their values
# becoming invalid due to a change in the original declaration.
@property
def declaration(self):
'''
:returns: the original declaration of the symbol.
:rtype: str
'''
return self._declaration
[docs]
class UnsupportedFortranType(UnsupportedType):
'''Indicates that a Fortran declaration is not supported by the PSyIR.
:param str declaration_txt: string containing the original variable
declaration.
:param partial_datatype: a PSyIR datatype representing the subset
of type attributes that are supported.
:type partial_datatype: Optional[
:py:class:`psyclone.psyir.symbols.DataType` or
:py:class:`psyclone.psyir.symbols.DataTypeSymbol`]
'''
def __init__(self, declaration_txt, partial_datatype=None):
super().__init__(declaration_txt)
# This will hold the Fortran type specification (as opposed to
# the whole declaration).
self._type_text = ""
if not isinstance(
partial_datatype, (type(None), DataType, DataTypeSymbol)):
raise TypeError(
f"partial_datatype argument in UnsupportedFortranType "
f"initialisation should be a DataType, DataTypeSymbol, or "
f"NoneType, but found '{type(partial_datatype).__name__}'.")
# This holds a subset of the type in a datatype if it is
# possible to determine enough information to create one.
self._partial_datatype = partial_datatype
def __str__(self):
return f"UnsupportedFortranType('{self._declaration}')"
@property
def partial_datatype(self):
'''
:returns: partial datatype information if it can be determined,
else None.
:rtype: Optional[:py:class:`psyclone.psyir.symbols.DataType` |
:py:class:`psyclone.symbols.symbols.DataTypeSymbol`]
'''
return self._partial_datatype
@property
def type_text(self):
'''
Parses the original Fortran declaration and uses the resulting
parse tree to extract the type information. This is returned in
text form and also cached.
TODO #2137 - alter Unsupported(Fortran)Type so that it is only the
type information that is stored as a string. i.e. remove the name
of the variable being declared. Once that is done this method
won't be required.
Note that UnsupportedFortranType is also used to hold things like
'SAVE :: /my_common/' and thus it is not always possible/appropriate
to extract a type expression.
:returns: the Fortran code specifying the type.
:rtype: str
:raises NotImplementedError: if declaration text cannot be
extracted from the original Fortran declaration.
'''
if self._type_text:
return self._type_text
# Encapsulate fparser2 functionality here.
# pylint:disable=import-outside-toplevel
from fparser.common.readfortran import FortranStringReader
from fparser.common.sourceinfo import FortranFormat
from fparser.two import Fortran2003
from fparser.two.parser import ParserFactory
string_reader = FortranStringReader(self._declaration)
# Set reader to free format.
string_reader.set_format(FortranFormat(True, False))
ParserFactory().create(std=Config.get().fortran_standard)
try:
ptree = Fortran2003.Specification_Part(
string_reader)
except Exception as err:
raise NotImplementedError(
f"Cannot extract the declaration part from UnsupportedFortran"
f"Type '{self._declaration}' because parsing (attempting to "
f"match a Fortran2003.Specification_Part) failed.") from err
node = ptree.children[0]
if isinstance(node, (Fortran2003.Declaration_Construct,
Fortran2003.Type_Declaration_Stmt)):
self._type_text = str(node.children[0])
elif isinstance(node, Fortran2003.Save_Stmt):
self._type_text = "SAVE"
elif isinstance(node, Fortran2003.Common_Stmt):
self._type_text = "COMMON"
else:
raise NotImplementedError(
f"Cannot extract the declaration part from UnsupportedFortran"
f"Type '{self._declaration}'. Only Declaration_Construct, "
f"Type_Declaration_Stmt, Save_Stmt and Common_Stmt are "
f"supported but got '{type(node).__name__}' from the parser.")
return self._type_text
def __eq__(self, other):
'''
:param Any other: the object to check equality to.
:returns: whether this type is equal to the 'other' type.
:rtype: bool
'''
if not super().__eq__(other):
return False
return other.type_text == self.type_text
[docs]
def copy(self):
'''
:returns: a copy of this datatype.
:rtype: :py:class:`psyclone.psyir.symbols.datatypes.UnknownFortranType`
'''
new = copy.copy(self)
if self._partial_datatype:
new._partial_datatype = self._partial_datatype.copy()
return new
[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.
This base implementation simply propagates the call to any child Nodes.
: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`
'''
if self.partial_datatype:
self.partial_datatype.replace_symbols_using(table_or_symbol)
@property
def intrinsic(self):
'''
:returns: the intrinsic used by this type (this is often recoverable
from the partial datatype).
:rtype: :py:class:`pyclone.psyir.datatypes.ScalarType.Intrinsic`
'''
if self.partial_datatype:
return self.partial_datatype.intrinsic
return None
[docs]
def get_all_accessed_symbols(self) -> set[Symbol]:
'''
:returns: a set of all the symbols accessed inside this DataType.
'''
symbols = super().get_all_accessed_symbols()
if self.partial_datatype:
if isinstance(self.partial_datatype, DataTypeSymbol):
symbols.add(self.partial_datatype)
else:
symbols.update(
self.partial_datatype.get_all_accessed_symbols())
return symbols
@property
def is_allocatable(self) -> Optional[bool]:
'''If we have enough information in the partial_datatype,
determines whether this data type is allocatable or not.
If it is unknown, it will return None. Note that atm PSyclone
only supports the allocatable attribute for **arrays**.
# TODO #2898 If we support non-array allocatable types, the
test for arrays can be removed
:returns: whether this UnsupportedFortranType is known to be
allocatable.'''
if (self.partial_datatype and
isinstance(self.partial_datatype, ArrayType)):
return self.partial_datatype.is_allocatable
return None
[docs]
class ScalarType(DataType):
'''Describes a scalar datatype (and its precision).
:param intrinsic: the intrinsic of this scalar type.
:param precision: the precision of this scalar type.
:param length: optionally, the length of a character type.
:raises TypeError: if any of the arguments are of the wrong type.
:raises ValueError: if any of the argument have unexpected values.
'''
[docs]
class ScalarTypeAttribute(Enum):
'''
Provides some common functionality to the various classes that
describe attributes of a ScalarType.
'''
[docs]
def copy(self) -> ScalarType.ScalarTypeAttribute:
''':returns: a copy of self.'''
return copy.copy(self)
[docs]
def debug_string(self) -> str:
''':returns: the name of the Enum item.'''
return self.name
[docs]
class Intrinsic(ScalarTypeAttribute):
'''Enumeration of the different intrinsic scalar datatypes that are
supported by the PSyIR.
'''
INTEGER = 1
REAL = 2
BOOLEAN = 3
CHARACTER = 4
[docs]
class Precision(ScalarTypeAttribute):
'''Enumeration of the different types of 'default' precision that may
be specified for a scalar datatype.
'''
SINGLE = 1
DOUBLE = 2
UNDEFINED = 3
[docs]
class CharLengthParameter(ScalarTypeAttribute):
'''Enumeration of different length characteristics that a character
type may have.
'''
#: The length is defined by some other variable. In Fortran
## this is indicated with an asterisk.
ASSUMED = 1
#: The length can change during program execution. In Fortran this
## is indicated with a colon.
DEFERRED = 2
#: Mapping from PSyIR scalar data types to intrinsic Python types
#: ignoring precision.
TYPE_MAP_TO_PYTHON = {
Intrinsic.INTEGER: int,
Intrinsic.CHARACTER: str,
Intrinsic.BOOLEAN: bool,
Intrinsic.REAL: float}
def __init__(
self,
intrinsic: ScalarType.Intrinsic,
precision: Union[int, ScalarType.Precision, "DataNode"],
length: Optional[
Union[int, ScalarType.CharLengthParam, "DataNode"]] = None
):
if not isinstance(intrinsic, ScalarType.Intrinsic):
raise TypeError(
f"ScalarType expected 'intrinsic' argument to be of type "
f"ScalarType.Intrinsic but found "
f"'{type(intrinsic).__name__}'.")
self._intrinsic = intrinsic
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.datanode import DataNode
if not isinstance(precision, (DataNode, ScalarType.Precision, int)):
raise TypeError(
f"ScalarType expected 'precision' argument to be of type "
f"DataNode, int or ScalarType.Precision, "
f"but found '{type(precision).__name__}'.")
if isinstance(precision, int) and precision <= 0:
raise ValueError(
f"The precision of a DataSymbol when specified as an integer "
f"number of bytes must be > 0 but found '{precision}'.")
if isinstance(precision, DataNode):
dtype = precision.datatype
if (not (isinstance(dtype, ScalarType) and
dtype.intrinsic ==
ScalarType.Intrinsic.INTEGER) and
not isinstance(dtype, UnresolvedType)):
raise ValueError(
f"A DataNode representing the precision of another "
f"DataSymbol must be of either 'unresolved' or "
f"scalar, integer type but got: ScalarType with "
f"datatype {dtype}")
# TODO #3135 If the precision is an int, then we would like to make
# a Literal containing it instead, however this is not currently
# possible due to circular imports.
self._precision = precision
# The 'length' setter includes validation checks.
self._length = None
self.length = length
@property
def length(self) -> "DataNode":
'''
:returns: the length of a character type.
:raises TypeError: if this ScalarType instance is not of
character type.
'''
if self._intrinsic != ScalarType.Intrinsic.CHARACTER:
raise TypeError(
f"A ScalarType of intrinsic type '{self._intrinsic}' does not "
f"have the 'length' property.")
return self._length
@length.setter
def length(self, value: Union[int, "DataNode", None]):
'''
Setter for the length of a character string. If the new value
is supplied as an int then this is converted into a Literal.
If this type is a character string and the `value` is None then
the length is set to the Fortran default of 1.
:value: the new length to assign.
:raises TypeError: if value is not None and this is not a
character type.
:raises ValueError: if the supplied value is an int with value < 0.
:raises TypeError: if the supplied value is of the wrong type.
'''
if self._intrinsic != ScalarType.Intrinsic.CHARACTER:
if value is None:
self._length = None
return
raise TypeError(
f"Only ScalarTypes of character type support the length "
f"property but length '{value}' was supplied to an intrinsic"
f" type of '{self._intrinsic}'")
# This is a character type.
if value is None:
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.literal import Literal
# Default length of a character string is 1.
self._length = Literal("1", ScalarType.integer_type())
return
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.datanode import DataNode
if isinstance(value, ScalarType.CharLengthParameter):
self._length = value
elif isinstance(value, int) and not isinstance(value, bool):
if value < 0:
raise ValueError(
f"If the length of a character ScalarType is specified "
f"using an int then it must be >= 0 but got: {value}")
from psyclone.psyir.nodes.literal import Literal
self._length = Literal(str(value), ScalarType.integer_type())
elif isinstance(value, DataNode):
self._length = value
else:
raise TypeError(
f"The length property of a character ScalarType must be a non-"
f"negative int, ScalarType.CharLengthParameter "
f"or DataNode but got '{type(value).__name__}'")
@property
def intrinsic(self) -> ScalarType.Intrinsic:
'''
:returns: the intrinsic used by this scalar type.
'''
return self._intrinsic
@property
def precision(self):
'''
:returns: the precision of this scalar type.
:rtype: :py:class:`psyclone.psyir.symbols.ScalarType.Precision` |
int | :py:class:`psyclone.psyir.nodes.DataNode`
'''
return self._precision
def __str__(self) -> str:
'''
:returns: a description of this scalar datatype.
'''
if isinstance(self.precision, ScalarType.Precision):
precision_str = self.precision.name
else:
precision_str = str(self.precision)
if self._length:
len_str = f", len:{self._length}"
else:
len_str = ""
return f"Scalar<{self.intrinsic.name}, {precision_str}{len_str}>"
def __eq__(self, other: Any) -> bool:
'''
:param other: the object to check equality to.
:returns: whether this type is equal to the 'other' type.
'''
if not super().__eq__(other):
return False
if self.intrinsic != other.intrinsic:
return False
# TODO #2659 - the following should be sufficient but isn't because
# currently, each new instance of an LFRicIntegerScalarDataType ends
# up with a brand new instance of a precision symbol.
# return (self.precision == other.precision and
# self.intrinsic == other.intrinsic)
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.reference import Reference
if (isinstance(other.precision, Reference) and
isinstance(self.precision, Reference)):
precision_match = (
other.precision.symbol.name == self.precision.symbol.name
and other.precision.symbol.interface ==
self.precision.symbol.interface
)
else:
precision_match = self.precision == other.precision
if self.intrinsic == ScalarType.Intrinsic.CHARACTER:
# We've already checked that the two are of the same intrinsic type
length_match = self._length == other.length
else:
length_match = True
return precision_match and length_match
[docs]
def replace_symbols_using(
self,
table_or_symbol: Union[SymbolTable, Symbol]) -> None:
'''
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.
'''
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.datanode import DataNode
if isinstance(self.precision, DataNode):
self._precision.replace_symbols_using(table_or_symbol)
if isinstance(self._length, DataNode):
self._length.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 DataType.
'''
symbols = super().get_all_accessed_symbols()
# Avoid circular import
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes.datanode import DataNode
if isinstance(self.precision, DataNode):
symbols.update(self.precision.get_all_accessed_symbols())
if isinstance(self._length, DataNode):
symbols.update(self._length.get_all_accessed_symbols())
return symbols
[docs]
def copy(self) -> ScalarType:
'''
:returns: a copy of self.
'''
if isinstance(self.precision, int):
# TODO #3135 - ideally precision will always be stored as a
# DataNode and this branch of the `if` won't be necessary.
precision = self.precision
else:
precision = self.precision.copy()
if self._length:
return ScalarType(self.intrinsic, precision, self._length.copy())
return ScalarType(self.intrinsic, precision)
# Create common scalar datatypes
[docs]
@staticmethod
def real_type() -> "ScalarType":
''' :returns: a REAL scalartype '''
return ScalarType(ScalarType.Intrinsic.REAL,
ScalarType.Precision.UNDEFINED)
[docs]
@staticmethod
def real_single_type() -> "ScalarType":
''' :returns: a REAL single scalartype '''
return ScalarType(ScalarType.Intrinsic.REAL,
ScalarType.Precision.SINGLE)
[docs]
@staticmethod
def real_double_type() -> "ScalarType":
''' :returns: a REAL double scalartype '''
return ScalarType(ScalarType.Intrinsic.REAL,
ScalarType.Precision.DOUBLE)
[docs]
@staticmethod
def real4_type() -> "ScalarType":
''' :returns: a REAL 4-byte scalartype '''
return ScalarType(ScalarType.Intrinsic.REAL, 4)
[docs]
@staticmethod
def real8_type() -> "ScalarType":
''' :returns: a REAL 8-byte scalartype '''
return ScalarType(ScalarType.Intrinsic.REAL, 8)
[docs]
@staticmethod
def integer_type() -> "ScalarType":
''' :returns: a INTEGER scalartype '''
return ScalarType(ScalarType.Intrinsic.INTEGER,
ScalarType.Precision.UNDEFINED)
[docs]
@staticmethod
def integer_single_type() -> "ScalarType":
''' :returns: a INTEGER single scalartype '''
return ScalarType(ScalarType.Intrinsic.INTEGER,
ScalarType.Precision.SINGLE)
[docs]
@staticmethod
def integer_double_type() -> "ScalarType":
''' :returns: a INTEGER double scalartype '''
return ScalarType(ScalarType.Intrinsic.INTEGER,
ScalarType.Precision.DOUBLE)
[docs]
@staticmethod
def integer4_type() -> "ScalarType":
''' :returns: a INTEGER 4-byte scalartype '''
return ScalarType(ScalarType.Intrinsic.INTEGER, 4)
[docs]
@staticmethod
def integer8_type() -> "ScalarType":
''' :returns: a INTEGER 8-byte scalartype '''
return ScalarType(ScalarType.Intrinsic.INTEGER, 8)
[docs]
@staticmethod
def boolean_type() -> "ScalarType":
''' :returns: a BOOLEAN scalartype '''
return ScalarType(ScalarType.Intrinsic.BOOLEAN,
ScalarType.Precision.UNDEFINED)
[docs]
@staticmethod
def character_type() -> "ScalarType":
''' :returns: a BOOLEAN scalartype '''
return ScalarType(ScalarType.Intrinsic.CHARACTER,
ScalarType.Precision.UNDEFINED, 1)
[docs]
class ArrayType(DataType):
'''Describes an array datatype. Can be an array of intrinsic types (e.g.
integer) or of structure types. For the latter, the type must currently be
specified as a DataTypeSymbol (see #1031).
:param elemental_type: the datatype of the array elements.
:param list shape: shape of the symbol in column-major order (leftmost
index is contiguous in memory). Each entry represents an array
dimension. If it is ArrayType.Extent.ATTRIBUTE the extent of that
dimension is unknown but the lower bound is 1 and the extent can be
obtained by querying the appropriate intrinsic (e.g. using the SIZE
intrinsic in Fortran). If it is ArrayType.Extent.DEFERRED then the
extent and bounds are all unknown and may or may not be possible to
query them using intrinsics (e.g. the array is ALLOCATABLE in Fortran).
Otherwise it can be an int or a DataNode (that returns an
int) or a 2-tuple of such quantities. If only a single value is
provided then that is taken to be the upper bound and the lower bound
defaults to 1. If a 2-tuple is provided then the two members specify
the lower and upper bounds, respectively, of the current dimension.
Note that providing an int is supported as a convenience, the provided
value will be stored internally as a Literal node.
:raises TypeError: if the arguments are of the wrong type.
:raises NotImplementedError: if a structure type does not have a
DataTypeSymbol as its type.
'''
[docs]
class Extent(Enum):
'''
Enumeration of array shape extents that are unspecified at compile
time. An 'ATTRIBUTE' extent means that the lower bound is known
(defaults to 1 if not specified) with an unknown extent (which can be
retrieved at run-time with the UBOUND intrinsic). A 'DEFERRED' extent
means that we don't know anything about the bounds, and run-time
intrinsics may or may not be able to retrieve them (e.g. the array may
need to be allocated/malloc'd).
'''
DEFERRED = 1
ATTRIBUTE = 2
[docs]
def copy(self):
'''
:returns: a copy of self.
:rtype: :py:class:`psyclone.psyir.symbols.ArrayType.Extent`
'''
return copy.copy(self)
[docs]
def get_all_accessed_symbols(self) -> set[Symbol]:
'''
:returns: a set of all the symbols accessed inside this Extent.
'''
return set()
[docs]
@dataclass(frozen=True)
class ArrayBounds:
'''
Class to store lower and upper limits of a declared array dimension.
:param lower: the lower bound of the array dimension.
:type lower: :py:class:`psyclone.psyir.nodes.DataNode`
:param upper: the upper bound of the array dimension or
ArrayType.Extent.ATTRIBUTE if unspecified.
:type upper: Union[:py:class:`psyclone.psyir.nodes.DataNode`,
`psyclone.psyir.symbols.datatypes.ArrayType.Extent.ATTRIBUTE`]
'''
# Have to use Any here as using DataNode causes a circular dependence.
lower: Any
upper: Any
def __post_init__(self):
'''
Adds validation of the values provided to the constructor.
:raises TypeError: if either bound is not a DataNode (or
ArrayType.Extent.ATTRIBUTE for the upper bound).
'''
# This import must be placed here to avoid circular dependencies.
# pylint: disable-next=import-outside-toplevel
from psyclone.psyir.nodes import Assignment, DataNode
def _dangling_parent(
node: Union[DataNode, ArrayType.Extent]
) -> Union[DataNode, ArrayType.Extent]:
''' Helper routine that copies and adds a dangling parent
Assignment to a given node, this implicitly guarantees that the
node is not attached anywhere else (and is unexpectedly
modified) and also makes it behave like other nodes (e.g. calls
inside an expression do not have the "call" keyword in Fortran)
:param node: The given bound.
:returns: the node with dangling parent when necessary.
'''
if isinstance(node, DataNode):
parent = Assignment()
parent.addchild(node.copy())
return parent.children[0]
return node
if not isinstance(self.lower, DataNode):
raise TypeError(
f"The lower bound provided when constructing an "
f"ArrayBounds must be an instance of DataNode but got "
f"'{type(self.lower).__name__}'")
if (self.upper != ArrayType.Extent.ATTRIBUTE and
not isinstance(self.upper, DataNode)):
raise TypeError(
f"The upper bound provided when constructing an "
f"ArrayBounds must be either ArrayType.Extent.ATTRIBUTE or"
f" an instance of DataNode but got "
f"'{type(self.upper).__name__}'")
# setattr necessary to bypass frozen dataclass restrictions
object.__setattr__(self, 'lower', _dangling_parent(self.lower))
object.__setattr__(self, 'upper', _dangling_parent(self.upper))
[docs]
def copy(self) -> ArrayType.ArrayBounds:
'''
:returns: a copy of this ArrayBounds object.
'''
return ArrayType.ArrayBounds(
self.lower.copy(), self.upper.copy()
)
def __init__(
self,
elemental_type: Union[DataType, DataTypeSymbol],
shape
):
# This import must be placed here to avoid circular dependencies.
# pylint: disable-next=import-outside-toplevel
from psyclone.psyir.nodes import Literal, DataNode
def _node_from_int(var):
''' Helper routine that simply creates a Literal out of an int.
If the supplied arg is not an int then it is returned unchanged.
:param var: variable for which to create a Literal if necessary.
:type var: int | :py:class:`psyclone.psyir.nodes.DataNode` | Extent
:returns: the variable with ints converted to DataNodes.
:rtype: :py:class:`psyclone.psyir.nodes.DataNode` | Extent
'''
if isinstance(var, int):
return Literal(str(var), ScalarType.integer_type())
return var
if isinstance(elemental_type, DataType):
if isinstance(elemental_type, StructureType):
# TODO #1031 remove this restriction.
raise NotImplementedError(
"When creating an array of structures, the type of "
"those structures must be supplied as a DataTypeSymbol "
"but got a StructureType instead.")
elif isinstance(elemental_type, DataTypeSymbol):
pass
else:
raise TypeError(
f"ArrayType expected 'elemental_type' argument to be of type "
f"DataType or DataTypeSymbol but found "
f"'{type(elemental_type).__name__}'.")
# We do not have a setter for shape as it is an immutable property,
# therefore we have a separate validation routine.
self._validate_shape(shape)
# Replace any ints in shape with a Literal. int's are only supported
# as they allow a more concise dimension declaration.
self._shape = []
one = Literal("1", ScalarType.integer_type())
for dim in shape:
if isinstance(dim, (DataNode, int)):
# The lower bound is 1 by default.
self._shape.append(
ArrayType.ArrayBounds(one, _node_from_int(dim)))
elif isinstance(dim, tuple):
self._shape.append(
ArrayType.ArrayBounds(_node_from_int(dim[0]),
_node_from_int(dim[1])))
else:
self._shape.append(dim)
self._elemental_type = elemental_type
@property
def elemental_type(self) -> Union[DataType, DataTypeSymbol]:
'''
:returns: the datatype of each element in the array.
'''
return self._elemental_type
@property
def intrinsic(self) -> Union[
DataType, DataTypeSymbol, ScalarType.Intrinsic
]:
'''
:returns: the intrinsic type of each element in the array.
'''
if isinstance(self._elemental_type,
(DataTypeSymbol, UnresolvedType, UnsupportedType)):
return self._elemental_type
return self._elemental_type.intrinsic
@property
def precision(self) -> Union[None, ScalarType.Precision, int, DataSymbol]:
'''
:returns: the precision of each element in the array.
'''
if isinstance(self._elemental_type,
(DataTypeSymbol, UnresolvedType, UnsupportedType)):
return None
return self._elemental_type.precision
@property
def is_allocatable(self) -> bool:
'''
:returns: whether this array is allocatable or not.
'''
# A 'deferred' array extent means this is an allocatable array
return ArrayType.Extent.DEFERRED in self.shape
@property
def shape(self):
'''
:returns: the (validated) shape of the symbol in column-major order
(leftmost index is contiguous in memory) with each entry
representing an array dimension.
If an entry is ArrayType.Extent.ATTRIBUTE the extent of that
dimension is unknown but can be obtained by querying the run-time
system (e.g. using the SIZE intrinsic in Fortran). If it is
ArrayType.Extent.DEFERRED then the extent is also unknown and may
or may not be defined at run-time (e.g. the array is ALLOCATABLE
in Fortran). Otherwise an entry is an ArrayBounds namedtuple with
lower and upper components.
:rtype: list[ArrayType.Extent.ATTRIBUTE |
ArrayType.Extent.DEFERRED |
:py:class:`psyclone.psyir.nodes.ArrayType.ArrayBounds`].
'''
self._validate_shape(self._shape)
return self._shape
def _validate_shape(self, extents):
'''Check that the supplied shape specification is valid. This is not
implemented as a setter because the shape property is immutable.
:param extents: list of extents, one for each array dimension.
:type extents: List[
:py:class:`psyclone.psyir.symbols.ArrayType.Extent` | int
| :py:class:`psyclone.psyir.nodes.DataNode` |
Tuple[int | :py:class:`psyclone.psyir.nodes.DataNode` |
:py:class:`psyclone.psyir.symbols.ArrayType.Extent`]]
:raises TypeError: if extents is not a list.
:raises TypeError: if one or more of the supplied extents is a
DataSymbol that is not a scalar integer or of
Unsupported/UnresolvedType.
:raises TypeError: if one or more of the supplied extents is not an
int, ArrayType.Extent or DataNode.
:raises TypeError: if the extents contain an invalid combination of
ATTRIBUTE/DEFERRED and known limits.
'''
# This import must be placed here to avoid circular
# dependencies.
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import DataNode, Reference
def _validate_data_node(dim_node, is_lower_bound=False):
'''
Checks that the supplied DataNode is valid as part of an
array-shape specification.
:param dim_node: the node to check for validity.
:type dim_node: int | :py:class:`psyclone.psyir.nodes.DataNode`
:param bool is_lower_bound: whether the supplied node represents \
the lower bound of an array shape.
:raises TypeError: if the DataNode is not valid in this context.
'''
if isinstance(dim_node, int):
# An integer value is always valid.
return
# When issue #1799 is addressed then check that the
# datatype returned is an int (or is Unsupported). For the
# moment, just check that if the DataNode is a
# Reference then the associated symbol is a scalar
# integer or is Unsupported.
if isinstance(dim_node, Reference):
# Check the DataSymbol instance is a scalar
# integer or is unsupported
dtype = dim_node.datatype
if isinstance(dtype, ArrayType) and dtype.shape:
raise TypeError(
f"If a DataSymbol is referenced in a dimension "
f"declaration then it should be a scalar but "
f"'{dim_node}' is not.")
if not (isinstance(dtype, (UnsupportedType, UnresolvedType)) or
dtype.intrinsic == ScalarType.Intrinsic.INTEGER):
raise TypeError(
f"If a DataSymbol is referenced in a dimension "
f"declaration then it should be an integer or "
f"of UnsupportedType or UnresolvedType, but "
f"'{dim_node.name}' is a '{dtype}'.")
# TODO #1089 - add check that any References are not to a
# local datasymbol that is not constant (as this would have
# no value).
return
if isinstance(dim_node, ArrayType.Extent):
if is_lower_bound:
raise TypeError(
"If present, the lower bound in an ArrayType 'shape' "
"must represent a value but found ArrayType.Extent")
return
if isinstance(dim_node, DataNode):
return
raise TypeError(
f"ArrayType shape-list elements can only be 'int', "
f"ArrayType.Extent, 'DataNode' or a 2-tuple thereof but found "
f"'{type(dim_node).__name__}'.")
# ---------------------------------------------------------------------
if not isinstance(extents, list):
raise TypeError(
f"ArrayType 'shape' must be of type list but found "
f"'{type(extents).__name__}'.")
for dimension in extents:
if isinstance(dimension, tuple):
if len(dimension) != 2:
raise TypeError(
f"An ArrayType shape-list element specifying lower "
f"and upper bounds must be a 2-tuple but "
f"'{dimension}' has {len(dimension)} entries.")
_validate_data_node(dimension[0], is_lower_bound=True)
_validate_data_node(dimension[1])
elif isinstance(dimension, ArrayType.ArrayBounds):
_validate_data_node(dimension.lower, is_lower_bound=True)
_validate_data_node(dimension.upper)
else:
_validate_data_node(dimension)
if ArrayType.Extent.DEFERRED in extents:
if not all(dim == ArrayType.Extent.DEFERRED
for dim in extents):
raise TypeError(
f"A declaration of an allocatable array must have"
f" the extent of every dimension as 'DEFERRED' but "
f"found shape: {extents}.")
if ArrayType.Extent.ATTRIBUTE in extents:
# If we have an 'assumed-shape' array then *every*
# dimension must have an 'ATTRIBUTE' extent
for dim in extents:
if not (dim == ArrayType.Extent.ATTRIBUTE or
(isinstance(dim, tuple) and
dim[-1] == ArrayType.Extent.ATTRIBUTE) or
(isinstance(dim, ArrayType.ArrayBounds) and
dim.upper == ArrayType.Extent.ATTRIBUTE)):
raise TypeError(
f"An assumed-shape array must have every "
f"dimension unspecified (either as 'ATTRIBUTE' or "
f"with the upper bound as 'ATTRIBUTE') but found "
f"shape: {extents}.")
def __str__(self):
'''
:returns: a description of this array datatype. If the lower bound \
of any dimension has the default value of 1 then it is omitted \
for the sake of brevity.
:rtype: str
:raises InternalError: if the shape of this datatype contains \
any elements that aren't ArrayBounds or ArrayType.Extent objects.
'''
dims = []
for dimension in self.shape:
if isinstance(dimension, ArrayType.ArrayBounds):
dim_text = ""
# Have to import locally to avoid circular dependence
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import Literal
# Lower bound. If it is "1" then we omit it.
if isinstance(dimension.lower, Literal):
if dimension.lower.value != "1":
dim_text = dimension.lower.value + ":"
else:
dim_text = str(dimension.lower) + ":"
# Upper bound.
if isinstance(dimension.upper, Literal):
dim_text += dimension.upper.value
else:
dim_text += str(dimension.upper)
dims.append(dim_text)
elif isinstance(dimension, ArrayType.Extent):
dims.append(f"'{dimension.name}'")
else:
raise InternalError(
f"Once constructed, every member of an ArrayType shape-"
f"list should either be an ArrayBounds object or an "
f"instance of ArrayType.Extent but found "
f"'{type(dimension).__name__}'")
return f"Array<{self._elemental_type}, shape=[{', '.join(dims)}]>"
def __eq__(self, other):
'''
:param Any other: the object to check equality to.
:returns: whether this ArrayType is equal to the 'other' ArrayType.
:rtype: bool
'''
if not super().__eq__(other):
return False
if (self.intrinsic != other.intrinsic or
self.precision != other.precision):
return False
if len(self.shape) != len(other.shape):
return False
# TODO #1799 - this implementation currently has some limitations.
# e.g. a(1:10) and b(2:11) have the same datatype (an array of 1
# dimension and 10 elements) but we will currently return false.
# One improvement could be to use the SymbolicMath to do the comparison
# but this won't resolve all cases as shape can be references.
for this_dim, other_dim in zip(self.shape, other.shape):
if this_dim != other_dim:
return False
return True
[docs]
def copy(self):
'''
Create a copy of this ArrayType. Any shape expressions will be
re-created but any referenced Symbols will remain unchanged.
:returns: a copy of this ArrayType.
:rtype: :py:class:`psyclone.psyir.datatype.ArrayType`
'''
new_shape = []
current_shape = self.shape
for dim in current_shape:
if isinstance(dim, ArrayType.ArrayBounds):
new_bounds = ArrayType.ArrayBounds(dim.lower.copy(),
dim.upper.copy())
new_shape.append(new_bounds)
else:
# This dimension is specified with an ArrayType.Extent
# so no need to copy.
new_shape.append(dim)
# If we copy the ScalarType then we need to create a copy of it, as
# it can contain DataNodes, which must be copied.
if isinstance(self.elemental_type, ScalarType):
return ArrayType(self.elemental_type.copy(), new_shape)
# Otherwise we continue with this type's datatype, to handle cases
# such as a DataTypeSymbol datatype (which should not be copied).
return ArrayType(self.elemental_type, new_shape)
[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`
'''
if isinstance(self.elemental_type, DataTypeSymbol):
if isinstance(table_or_symbol, Symbol):
if table_or_symbol.name.lower() == \
self._elemental_type.name.lower():
self._elemental_type = table_or_symbol
else:
try:
self._elemental_type = \
table_or_symbol.lookup(self.elemental_type.name)
except KeyError:
pass
else:
self.elemental_type.replace_symbols_using(table_or_symbol)
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import Node
# Update any Symbols referenced in the array shape
for dim in self.shape:
if isinstance(dim, ArrayType.ArrayBounds):
exprns = [dim.lower, dim.upper]
else:
exprns = [dim]
for bnd in exprns:
if isinstance(bnd, Node):
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 DataType.
'''
symbols = super().get_all_accessed_symbols()
if not isinstance(self.elemental_type, Symbol):
symbols.update(self.elemental_type.get_all_accessed_symbols())
else:
symbols.add(self.elemental_type)
for dim in self.shape:
if isinstance(dim, ArrayType.ArrayBounds):
symbols.update(dim.lower.get_all_accessed_symbols())
symbols.update(dim.upper.get_all_accessed_symbols())
return symbols
[docs]
class StructureType(DataType):
'''
Describes a 'structure' or 'derived' datatype that is itself composed
of a list of other datatypes. Those datatypes are stored as an
OrderedDict of namedtuples.
Note, we could have chosen to use a SymbolTable to store the properties
of the constituents of the type. (Since they too have a name, a type,
and visibility.) If this class ends up duplicating a lot of the
SymbolTable functionality then this decision could be revisited.
'''
[docs]
@dataclass(frozen=True)
class ComponentType(CommentableMixin):
'''
Represents a member of a StructureType.
:param name: the name of the member.
:param datatype: the type of the member.
:param visibility: whether this member is public or private.
:param initial_value: the initial value of this member (if any).
:type initial_value: Optional[:py:class:`psyclone.psyir.nodes.Node`]
'''
name: str
datatype: Union[DataType, DataTypeSymbol]
visibility: Symbol.Visibility
initial_value: Any
def __init__(self):
self._components = OrderedDict()
def __str__(self):
return "StructureType<>"
def __copy__(self):
'''
:returns: a copy of this StructureType.
:rtype: :py:class:`psyclone.psyir.symbols.StructureType`
'''
new = StructureType()
for name, component in self.components.items():
new.add(name, component.datatype, component.visibility,
component.initial_value, component.preceding_comment,
component.inline_comment)
return new
[docs]
@staticmethod
def create(components):
'''
Creates a StructureType from the supplied list of properties.
:param components: the name, type, visibility (whether public or
private), initial value (if any), preceding comment (if any)
and inline comment (if any) of each component.
:type components: List[tuple[
str,
:py:class:`psyclone.psyir.symbols.DataType` |
:py:class:`psyclone.psyir.symbols.DataTypeSymbol`,
:py:class:`psyclone.psyir.symbols.Symbol.Visibility`,
Optional[:py:class:`psyclone.psyir.symbols.DataNode`],
Optional[str],
Optional[str]
]]
:returns: the new type object.
:rtype: :py:class:`psyclone.psyir.symbols.StructureType`
'''
stype = StructureType()
for component in components:
if len(component) not in (3, 4, 5, 6):
raise TypeError(
f"Each component must be specified using a 3 to 6-tuple "
f"of (name, type, visibility, initial_value, "
f"preceding_comment, inline_comment) but found a "
f"tuple with {len(component)} members: {component}")
stype.add(*component)
return stype
@property
def components(self):
'''
:returns: Ordered dictionary of the components of this type.
:rtype: :py:class:`collections.OrderedDict`
'''
return self._components
[docs]
def add(self, name: str, datatype, visibility, initial_value=None,
preceding_comment: str = "", inline_comment: str = ""):
'''
Create a component with the supplied attributes and add it to
this StructureType.
:param name: the name of the new component.
:param datatype: the type of the new component.
:type datatype: :py:class:`psyclone.psyir.symbols.DataType` |
:py:class:`psyclone.psyir.symbols.DataTypeSymbol`
:param visibility: whether this component is public or private.
:type visibility: :py:class:`psyclone.psyir.symbols.Symbol.Visibility`
:param initial_value: the initial value of the new component.
:type initial_value: Optional[
:py:class:`psyclone.psyir.nodes.DataNode`]
:param preceding_comment: a comment that precedes this component.
:param inline_comment: a comment that follows this component on the
same line.
:raises TypeError: if any of the supplied values are of the wrong type.
'''
# This import must be placed here to avoid circular
# dependencies.
# pylint: disable=import-outside-toplevel
from psyclone.psyir.nodes import DataNode
if not isinstance(name, str):
raise TypeError(
f"The name of a component of a StructureType must be a 'str' "
f"but got '{type(name).__name__}'")
if not isinstance(datatype, (DataType, DataTypeSymbol)):
raise TypeError(
f"The type of a component of a StructureType must be a "
f"'DataType' or 'DataTypeSymbol' but got "
f"'{type(datatype).__name__}'")
if not isinstance(visibility, Symbol.Visibility):
raise TypeError(
f"The visibility of a component of a StructureType must be "
f"an instance of 'Symbol.Visibility' but got "
f"'{type(visibility).__name__}'")
if datatype is self:
# A StructureType cannot contain a component of its own type
raise TypeError(
f"Error attempting to add component '{name}' - a "
f"StructureType definition cannot be recursive - i.e. it "
f"cannot contain components with the same type as itself.")
if (initial_value is not None and
not isinstance(initial_value, DataNode)):
raise TypeError(
f"The initial value of a component of a StructureType must "
f"be None or an instance of 'DataNode', but got "
f"'{type(initial_value).__name__}'.")
if not isinstance(preceding_comment, str):
raise TypeError(
f"The preceding_comment of a component of a StructureType "
f"must be a 'str' but got "
f"'{type(preceding_comment).__name__}'")
if not isinstance(inline_comment, str):
raise TypeError(
f"The inline_comment of a component of a StructureType must "
f"be a 'str' but got "
f"'{type(inline_comment).__name__}'")
key_name = name.lower()
self._components[key_name] = self.ComponentType(name, datatype,
visibility,
initial_value)
# Use object.__setattr__ due to the frozen nature of ComponentType
object.__setattr__(self._components[key_name],
"_preceding_comment",
preceding_comment)
object.__setattr__(self._components[key_name],
"_inline_comment",
inline_comment)
[docs]
def lookup(self, name):
'''
:returns: the ComponentType tuple describing the named member of this
StructureType.
:rtype: :py:class:`psyclone.psyir.symbols.StructureType.ComponentType`
'''
return self._components[name.lower()]
def __eq__(self, other):
'''
:param Any other: the object to check equality to.
:returns: whether this StructureType is equal to the 'other' type.
:rtype: bool
'''
if not super().__eq__(other):
return False
if len(self.components) != len(other.components):
return False
if self.components != other.components:
return False
return True
[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.
This base implementation simply propagates the call to any child Nodes.
: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`
'''
# Since ComponentType is a frozen dataclass it is immutable, therefore
# we must construct new ones.
for component in self.components.values():
if isinstance(component.datatype, DataTypeSymbol):
if isinstance(table_or_symbol, Symbol):
if (table_or_symbol.name.lower() ==
component.datatype.name.lower()):
new_type = table_or_symbol
else:
new_type = component.datatype
else:
new_type = table_or_symbol.lookup(
component.datatype.name, otherwise=component.datatype)
else:
component.datatype.replace_symbols_using(table_or_symbol)
new_type = component.datatype
if component.initial_value:
component.initial_value.replace_symbols_using(table_or_symbol)
# Construct the new ComponentType
key_name = component.name.lower()
self.add(key_name, new_type, component.visibility,
component.initial_value,
preceding_comment=component.preceding_comment,
inline_comment=component.inline_comment)
[docs]
def get_all_accessed_symbols(self) -> set[Symbol]:
'''
:returns: a set of all the symbols accessed inside this DataType.
'''
symbols = super().get_all_accessed_symbols()
for cmpt in self.components.values():
if isinstance(cmpt.datatype, DataTypeSymbol):
symbols.add(cmpt.datatype)
else:
symbols.update(cmpt.datatype.get_all_accessed_symbols())
if cmpt.initial_value:
symbols.update(
cmpt.initial_value.get_all_accessed_symbols())
return symbols
# For automatic documentation generation
__all__ = ["UnsupportedType", "UnsupportedFortranType", "UnresolvedType",
"ScalarType", "ArrayType", "StructureType"]