Source code for pyccel.ast.datatypes

# coding: utf-8

#------------------------------------------------------------------------------------------#
# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
# go to https://github.com/pyccel/pyccel/blob/devel/LICENSE for full license details.      #
#------------------------------------------------------------------------------------------#

"""
Classes and methods that handle supported datatypes in C/Fortran.
"""
from functools import lru_cache

import numpy

from pyccel.utilities.metaclasses import Singleton, ArgumentSingleton
from .basic import iterable

__all__ = (
        # ------------ Super classes ------------
        'ContainerType',
        'FixedSizeType',
        'PrimitiveType',
        'PyccelType',
        # ------------ Primitive types ------------
        'PrimitiveBooleanType',
        'PrimitiveCharacterType',
        'PrimitiveComplexType',
        'PrimitiveFloatingPointType',
        'PrimitiveIntegerType',
        # ------------ Fixed size types ------------
        'CharType',
        'FixedSizeNumericType',
        'GenericType',
        'PythonNativeBool',
        'PythonNativeComplex',
        'PythonNativeFloat',
        'PythonNativeInt',
        'PythonNativeNumericType',
        'SymbolicType',
        'TypeAlias',
        'VoidType',
        # ------------ Container types ------------
        'CustomDataType',
        'DictType',
        'HomogeneousContainerType',
        'HomogeneousListType',
        'HomogeneousSetType',
        'HomogeneousTupleType',
        'InhomogeneousTupleType',
        'StringType',
        'TupleType',
        # ---------- Functions -------------------
        'DataTypeFactory',
)

#==============================================================================
[docs] class PrimitiveType(metaclass=Singleton): """ Base class representing types of datatypes. The base class representing the category of datatype to which a FixedSizeType may belong (e.g. integer, floating point). """ __slots__ = () _name = '__UNDEFINED__' def __init__(self): #pylint: disable=useless-parent-delegation # This __init__ function is required so the ArgumentSingleton can # always detect a signature super().__init__() def __str__(self): return self._name
[docs] class PrimitiveBooleanType(PrimitiveType): """ Class representing a boolean datatype. Class representing a boolean datatype. """ __slots__ = () _name = 'boolean'
[docs] class PrimitiveIntegerType(PrimitiveType): """ Class representing an integer datatype. Class representing an integer datatype. """ __slots__ = () _name = 'integer'
[docs] class PrimitiveFloatingPointType(PrimitiveType): """ Class representing a floating point datatype. Class representing a floating point datatype. """ __slots__ = () _name = 'floating point'
[docs] class PrimitiveComplexType(PrimitiveType): """ Class representing a complex datatype. Class representing a complex datatype. """ __slots__ = () _name = 'complex'
[docs] class PrimitiveCharacterType(PrimitiveType): """ Class representing a character datatype. Class representing a character datatype. """ __slots__ = () _name = 'character'
#==============================================================================
[docs] class PyccelType: """ Base class representing the type of an object. Base class representing the type of an object from which all types must inherit. A type must contain enough information to describe the declaration type in a low-level language. Types contain an addition operator. The operator indicates the type that is expected when calling an arithmetic operator on objects of these types. Where applicable, types also contain an and operator. The operator indicates the type that is expected when calling a bitwise comparison operator on objects of these types. A type also contains an attribute _name which can be useful to examine the type. """ __slots__ = () _name = None @property def name(self): """ Get the name of the pyccel type. Get the name of the pyccel type. """ return self._name def __init__(self): #pylint: disable=useless-parent-delegation # This __init__ function is required so the ArgumentSingleton can # always detect a signature super().__init__() def __str__(self): return self._name #pylint: disable=no-member
[docs] def switch_basic_type(self, new_type): """ Change the basic type to the new type. Change the basic type to the new type. In the case of a FixedSizeType the switch will replace the type completely, directly returning the new type. In the case of a homogeneous container type, a new container type will be returned whose underlying elements are of the new type. This method is not implemented for inhomogeneous containers. Parameters ---------- new_type : PyccelType The new basic type. Returns ------- PyccelType The new type. """ raise NotImplementedError(f"switch_basic_type not implemented for {type(self)}")
[docs] def shape_is_compatible(self, shape): """ Check if the provided shape is compatible with the datatype. Check if the provided shape is compatible with the format expected for this datatype. Parameters ---------- shape : Any The proposed shape. Returns ------- bool True if the shape is acceptable, False otherwise. """ return shape is None
#==============================================================================
[docs] class FixedSizeType(PyccelType, metaclass=Singleton): """ Base class representing a built-in scalar datatype. The base class representing a built-in scalar datatype which can be represented in memory. E.g. int32, int64. """ __slots__ = () @property def datatype(self): """ The datatype of the object. The datatype of the object. """ return self @property def primitive_type(self): """ The datatype category of the object. The datatype category of the object (e.g. integer, floating point). """ return self._primitive_type # pylint: disable=no-member @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return 0 @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return None
[docs] def switch_basic_type(self, new_type): """ Change the basic type to the new type. Change the basic type to the new type. In the case of a FixedSizeType the switch will replace the type completely, directly returning the new type. Parameters ---------- new_type : PyccelType The new basic type. Returns ------- PyccelType The new type. """ assert isinstance(new_type, FixedSizeType) return new_type
[docs] class FixedSizeNumericType(FixedSizeType): """ Base class representing a scalar numeric datatype. The base class representing a scalar numeric datatype which can be represented in memory. E.g. int32, int64. """ __slots__ = () @property def precision(self): """ Precision of the datatype of the object. The precision of the datatype of the object. This number is related to the number of bytes that the datatype takes up in memory. For basic types the number is equivalent to the number of bytes in memory (e.g. `float64` has precision = 8 as it takes up 8 bytes), however for less simple types the connection is less trivial. For example `complex128` has precision = 8 as it is comprised of two `float64` objects (which have precision=8). It should be noted that this is not the convention chosen by NumPy (in NumPy a `complex128` is so named because `16*8=precision*bits_in_a_byte=128`). The precision in Pyccel is equivalent to the `kind` parameter in Fortran. """ return self._precision # pylint: disable=no-member
[docs] class PythonNativeNumericType(FixedSizeNumericType): """ Base class representing a built-in scalar numeric datatype. Base class representing a built-in scalar numeric datatype. """ __slots__ = ()
[docs] class PythonNativeBool(PythonNativeNumericType): """ Class representing Python's native boolean type. Class representing Python's native boolean type. """ __slots__ = () _name = 'bool' _primitive_type = PrimitiveBooleanType() _precision = -1 @lru_cache def __add__(self, other): if isinstance(other, PythonNativeBool): return PythonNativeInt() elif isinstance(other, PythonNativeNumericType): return other else: return NotImplemented @lru_cache def __and__(self, other): if isinstance(other, PythonNativeBool): return PythonNativeBool() elif isinstance(other, PythonNativeNumericType): return other else: return NotImplemented
[docs] class PythonNativeInt(PythonNativeNumericType): """ Class representing Python's native integer type. Class representing Python's native integer type. """ __slots__ = () _name = 'int' _primitive_type = PrimitiveIntegerType() _precision = numpy.dtype(int).alignment @lru_cache def __add__(self, other): if isinstance(other, PythonNativeBool): return self elif isinstance(other, PythonNativeNumericType): return other else: return NotImplemented @lru_cache def __and__(self, other): if isinstance(other, PythonNativeNumericType): return self else: return NotImplemented
[docs] class PythonNativeFloat(PythonNativeNumericType): """ Class representing Python's native floating point type. Class representing Python's native floating point type. """ __slots__ = () _name = 'float' _primitive_type = PrimitiveFloatingPointType() _precision = 8 @lru_cache def __add__(self, other): if isinstance(other, PythonNativeComplex): return other elif isinstance(other, PythonNativeNumericType): return self else: return NotImplemented
[docs] class PythonNativeComplex(PythonNativeNumericType): """ Class representing Python's native complex type. Class representing Python's native complex type. """ __slots__ = ('_element_type',) _name = 'complex' _primitive_type = PrimitiveComplexType() _precision = 8 @lru_cache def __add__(self, other): if isinstance(other, PythonNativeNumericType): return self else: return NotImplemented @property def element_type(self): """ The type of an element of the complex. The type of an element of the complex. In other words, the type of the floats which comprise the complex type. """ return PythonNativeFloat()
[docs] class VoidType(FixedSizeType): """ Class representing a void datatype. Class representing a void datatype. This class is especially useful in the C-Python wrapper when a `void*` type is needed to collect pointers from Fortran. """ __slots__ = () _name = 'void' _primitive_type = None
[docs] class GenericType(FixedSizeType): """ Class representing a generic datatype. Class representing a generic datatype. This datatype is useful for describing the type of an empty container (list/tuple/etc) or an argument which can accept any type (e.g. MPI arguments). """ __slots__ = () _name = 'Generic' _primitive_type = None @lru_cache def __add__(self, other): return other def __eq__(self, other): return True def __hash__(self): return hash(self.__class__)
[docs] class SymbolicType(FixedSizeType): """ Class representing the datatype of a placeholder symbol. Class representing the datatype of a placeholder symbol. This type should be used for objects which will not appear in the generated code but are used to identify objects (e.g. Type aliases). """ __slots__ = () _name = 'Symbolic' _primitive_type = None
[docs] class CharType(FixedSizeType): """ Class representing a char type in C/Fortran. Class representing a char type in C/Fortran. This datatype is useful for describing strings. """ __slots__ = () _name = 'char' _primitive_type = PrimitiveCharacterType()
#==============================================================================
[docs] class TypeAlias(SymbolicType): """ Class representing the type of a symbolic object describing a type descriptor. Class representing the type of a symbolic object describing a type descriptor. This type is equivalent to Python's built-in typing.TypeAlias. See Also -------- typing.TypeAlias : See documentation of `typing.TypeAlias`: https://docs.python.org/3/library/typing.html#typing.TypeAlias . """ __slots__ = () _name = 'TypeAlias'
#==============================================================================
[docs] class ContainerType(PyccelType): """ Base class representing a type which contains objects of other types. Base class representing a type which contains objects of other types. E.g. classes, arrays, etc. """ __slots__ = ()
[docs] def shape_is_compatible(self, shape): """ Check if the provided shape is compatible with the datatype. Check if the provided shape is compatible with the format expected for this datatype. Parameters ---------- shape : Any The proposed shape. Returns ------- bool True if the shape is acceptable, False otherwise. """ return isinstance(shape, tuple) and len(shape) == self.container_rank # pylint: disable=no-member
#==============================================================================
[docs] class TupleType: """ Base class representing tuple datatypes. The class from which tuple datatypes must inherit. """ __slots__ = () _name = 'tuple'
#==============================================================================
[docs] class HomogeneousContainerType(ContainerType): """ Base class representing a datatype which contains multiple elements of a given type. Base class representing a datatype which contains multiple elements of a given type. This is the case for objects such as arrays, lists, etc. """ __slots__ = () @property def datatype(self): """ The datatype of the object. The datatype of the object. """ return self.element_type.datatype @property def primitive_type(self): """ The datatype category of elements of the object. The datatype category of elements of the object (e.g. integer, floating point). """ return self.element_type.primitive_type @property def precision(self): """ Precision of the datatype of the object. The precision of the datatype of the object. This number is related to the number of bytes that the datatype takes up in memory. For basic types the number is equivalent to the number of bytes in memory (e.g. `float64` has precision = 8 as it takes up 8 bytes), however for less simple types the connection is less trivial. For example `complex128` has precision = 8 as it is comprised of two `float64` objects (which have precision=8). It should be noted that this is not the convention chosen by NumPy (in NumPy a `complex128` is so named because `16*8=precision*bits_in_a_byte=128`). The precision in Pyccel is equivalent to the `kind` parameter in Fortran. """ return self.element_type.precision @property def element_type(self): """ The type of elements of the object. The PyccelType describing an element of the container. """ return self._element_type # pylint: disable=no-member def __str__(self): return f'{self._name}[{self._element_type}]' # pylint: disable=no-member
[docs] def switch_basic_type(self, new_type): """ Change the basic type to the new type. Change the basic type to the new type. In the case of a FixedSizeType the switch will replace the type completely, directly returning the new type. In the case of a homogeneous container type, a new container type will be returned whose underlying elements are of the new type. This method is not implemented for inhomogeneous containers. Parameters ---------- new_type : PyccelType The new basic type. Returns ------- PyccelType The new type. """ assert isinstance(new_type, FixedSizeType) cls = type(self) return cls(self.element_type.switch_basic_type(new_type))
[docs] def switch_rank(self, new_rank, new_order = None): """ Get a type which is identical to this type in all aspects except the rank. Get a type which is identical to this type in all aspects except the rank. The order must be provided if the rank is increased from 1. This is never the case for 1D containers. Parameters ---------- new_rank : int The rank of the new type. new_order : str, optional The order of the new type. For 1D containers this should not be provided. Returns ------- PyccelType The new type. """ assert new_order is None rank = self.rank assert new_rank < rank if new_rank == rank: return self elif rank - new_rank == self.container_rank: return self.element_type else: return self.element_type.switch_rank(new_rank - self.container_rank)
@property def container_rank(self): """ Number of dimensions of the container. Number of dimensions of the object described by the container. This is equal to the number of values required to index an element of this container. """ return self._container_rank # pylint: disable=no-member @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return self.container_rank + self.element_type.rank @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return self._order # pylint: disable=no-member def __eq__(self, other): return isinstance(other, self.__class__) and self.element_type == other.element_type def __hash__(self): return hash((self.__class__, self.element_type))
[docs] class StringType(ContainerType, metaclass = Singleton): """ Class representing Python's native string type. Class representing Python's native string type. """ __slots__ = () _name = 'str' @property def datatype(self): """ The datatype of the object. The datatype of the object. """ return self def __str__(self): return 'str' @property def primitive_type(self): """ The datatype category of elements of the object. The datatype category of elements of the object (e.g. integer, floating point). """ return self @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return 1 @property def container_rank(self): """ Number of dimensions of the container. Number of dimensions of the object described by the container. This is equal to the number of values required to index an element of this container. """ return 1 @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return None @property def element_type(self): """ The type of elements of the object. The PyccelType describing an element of the container. """ return CharType() def __eq__(self, other): return isinstance(other, self.__class__) def __hash__(self): return hash(self.__class__)
[docs] class HomogeneousTupleType(HomogeneousContainerType, TupleType, metaclass = ArgumentSingleton): """ Class representing the homogeneous tuple type. Class representing the type of a homogeneous tuple. This is a container type and should be used as the class_type. Parameters ---------- element_type : PyccelType The type of the elements of the homogeneous tuple. """ _name = 'tuple' __slots__ = ('_element_type', '_order') _container_rank = 1 def __init__(self, element_type): assert isinstance(element_type, PyccelType) self._element_type = element_type self._order = 'C' if (element_type.order == 'C' or element_type.rank == 1) else None super().__init__() def __str__(self): return f'tuple[{self._element_type}, ...]'
[docs] def shape_is_compatible(self, shape): """ Check if the provided shape is compatible with the datatype. Check if the provided shape is compatible with the format expected for this datatype. Parameters ---------- shape : Any The proposed shape. Returns ------- bool True if the shape is acceptable, False otherwise. """ # TODO: Remove this specialisation if tuples are saved in lists instead of ndarrays return isinstance(shape, tuple) and len(shape) == self.rank
[docs] class HomogeneousListType(HomogeneousContainerType, metaclass = ArgumentSingleton): """ Class representing the homogeneous list type. Class representing the type of a homogeneous list. This is a container type and should be used as the class_type. Parameters ---------- element_type : PyccelType The type which is stored in the homogeneous list. """ __slots__ = ('_element_type', '_order') _name = 'list' _container_rank = 1 def __init__(self, element_type): assert isinstance(element_type, PyccelType) self._element_type = element_type self._order = 'C' if (element_type.order == 'C' or element_type.rank == 1) else None super().__init__() def __eq__(self, other): return isinstance(other, self.__class__) and self._element_type == other._element_type \ and self._order == other._order def __hash__(self): return hash((self.__class__, self._element_type, self._order))
[docs] class HomogeneousSetType(HomogeneousContainerType, metaclass = ArgumentSingleton): """ Class representing the homogeneous set type. Class representing the type of a homogeneous set. This is a container type and should be used as the class_type. Parameters ---------- element_type : PyccelType The type which is stored in the homogeneous set. """ __slots__ = ('_element_type',) _name = 'set' _container_rank = 1 _order = None def __init__(self, element_type): assert isinstance(element_type, PyccelType) self._element_type = element_type super().__init__() def __eq__(self, other): return isinstance(other, self.__class__) and self._element_type == other._element_type def __hash__(self): return hash((self.__class__, self._element_type))
#==============================================================================
[docs] class CustomDataType(PyccelType, metaclass=Singleton): """ Class from which user-defined types inherit. A general class for custom data types which is used as a base class when a user defines their own type using classes. """ __slots__ = () @property def datatype(self): """ The datatype of the object. The datatype of the object. """ return self @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return 0 @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return None
[docs] class InhomogeneousTupleType(ContainerType, TupleType, metaclass = ArgumentSingleton): """ Class representing the inhomogeneous tuple type. Class representing the type of an inhomogeneous tuple. This is a basic datatype as it cannot be arbitrarily indexed. It is therefore parametrised by the datatypes that it contains. Parameters ---------- *args : tuple of DataTypes The datatypes stored in the inhomogeneous tuple. """ __slots__ = ('_element_types', '_datatype', '_container_rank', '_order') def __init__(self, *args): self._element_types = args # Determine datatype possible_types = set(t.datatype for t in self._element_types) dtype = possible_types.pop() self._datatype = dtype if all(d == dtype for d in possible_types) else self # Determine rank elem_ranks = set(elem.rank for elem in self._element_types) if len(elem_ranks) == 1: self._container_rank = elem_ranks.pop() + 1 else: self._container_rank = 1 # Determine order if self._container_rank == 2: self._order = 'C' elif self._container_rank > 2: elem_orders = set(elem.order for elem in self._element_types) if len(elem_orders) == 1 and elem_orders.pop() == 'C': self._order = 'C' else: self._order = None else: self._order = None super().__init__() def __str__(self): element_types = ', '.join(str(d) for d in self._element_types) return f'tuple[{element_types}]' def __getitem__(self, i): return self._element_types[i] def __len__(self): return len(self._element_types) def __iter__(self): return self._element_types.__iter__() @property def datatype(self): """ The datatype of the object. The datatype of the object. For an inhomogeneous tuple the datatype is the type of the tuple unless the tuple is comprised of containers which are all based on compatible data types. In this case one of the underlying types is returned. """ return self._datatype @property def container_rank(self): """ Number of dimensions of the container. Number of dimensions of the object described by the container. This is equal to the number of values required to index an element of this container. """ return 1 @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return self._container_rank @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return self._order
[docs] def shape_is_compatible(self, shape): """ Check if the provided shape is compatible with the datatype. Check if the provided shape is compatible with the format expected for this datatype. Parameters ---------- shape : Any The proposed shape. Returns ------- bool True if the shape is acceptable, False otherwise. """ return super().shape_is_compatible(shape) and shape[0] == len(self._element_types)
[docs] class DictType(ContainerType, metaclass = ArgumentSingleton): """ Class representing the homogeneous dictionary type. Class representing the type of a homogeneous dict. This is a container type and should be used as the class_type. Parameters ---------- key_type : PyccelType The type of the keys of the homogeneous dictionary. value_type : PyccelType The type of the values of the homogeneous dictionary. """ __slots__ = ('_key_type', '_value_type') _name = 'dict' _container_rank = 1 _order = None def __init__(self, key_type, value_type): self._key_type = key_type self._value_type = value_type super().__init__() def __str__(self): return f'dict[{self._key_type}, {self._value_type}]' @property def datatype(self): """ The datatype of the object. The datatype of the object. """ return self._key_type.datatype @property def key_type(self): """ The type of the keys of the object. The type of the keys of the object. """ return self._key_type @property def value_type(self): """ The type of the values of the object. The type of the values of the object. """ return self._value_type @property def container_rank(self): """ Number of dimensions of the container. Number of dimensions of the object described by the container. This is equal to the number of values required to index an element of this container. """ return 1 @property def rank(self): """ Number of dimensions of the object. Number of dimensions of the object. If the object is a scalar then this is equal to 0. """ return self._container_rank + self._value_type.rank @property def order(self): """ The data layout ordering in memory. Indicates whether the data is stored in row-major ('C') or column-major ('F') format. This is only relevant if rank > 1. When it is not relevant this function returns None. """ return None def __eq__(self, other): return isinstance(other, self.__class__) and self.key_type == other.key_type \ and self.value_type == other.value_type def __hash__(self): return hash((self.__class__, self._key_type, self._value_type))
#==============================================================================
[docs] def DataTypeFactory(name, argnames = (), *, BaseClass=CustomDataType): """ Create a new data class. Create a new data class which sub-classes a DataType. This provides a new data type which can be used, for example, for class types. Parameters ---------- name : str The name of the new class. argnames : iterable[str] A list of all the arguments for the new class. This can be used to create classes which are parametrised by a type. BaseClass : type inheriting from DataType The class from which the new type will be sub-classed. Returns ------- type A new DataType class. """ def class_init_func(self, **kwargs): """ The __init__ function for the new CustomDataType class. """ for key, value in kwargs.items(): # here, the argnames variable is the one passed to the # DataTypeFactory call if key not in argnames: raise TypeError(f"Argument {key} not valid for {self.__class__.__name__}") setattr(self, key, value) BaseClass.__init__(self) # pylint: disable=unnecessary-dunder-call assert iterable(argnames) assert all(isinstance(a, str) for a in argnames) def class_name_func(self): """ The name function for the new CustomDataType class. """ if argnames: param = ', '.join(str(getattr(self, a)) for a in argnames) return f'{self._name}[{param}]' #pylint: disable=protected-access else: return self._name #pylint: disable=protected-access newclass = type(f'Pyccel{name}', (BaseClass,), {"__init__": class_init_func, "name": property(class_name_func), "_name": name}) return newclass
#============================================================================== pyccel_type_to_original_type = { PythonNativeBool() : bool, PythonNativeInt() : int, PythonNativeFloat() : float, PythonNativeComplex() : complex, } original_type_to_pyccel_type = {v: k for k,v in pyccel_type_to_original_type.items()}