Source code for cocotb.types.logic_array

# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
import typing
import warnings

from cocotb._deprecation import deprecated
from cocotb.types import ArrayLike
from cocotb.types.logic import Logic, LogicConstructibleT
from cocotb.types.range import Range


[docs] class LogicArray(ArrayLike[Logic]): r""" Fixed-sized, arbitrarily-indexed, array of :class:`cocotb.types.Logic`. .. currentmodule:: cocotb.types :class:`LogicArray`\ s can be constructed from either iterables of values constructible into :class:`Logic`: like :class:`bool`, :class:`str`, :class:`int`. Like :class:`Array`, if no *range* argument is given, it is deduced from the length of the iterable used to initialize the variable. If a *range* argument is given, but no value, the array is filled with the default value of ``Logic()``. .. code-block:: python3 >>> LogicArray("01XZ") LogicArray('01XZ', Range(3, 'downto', 0)) >>> LogicArray([0, True, "X"]) LogicArray('01X', Range(2, 'downto', 0)) >>> LogicArray(range=Range(0, "to", 3)) # default values LogicArray('XXXX', Range(0, 'to', 3)) :class:`LogicArray`\ s can be constructed from :class:`int`\ s using :meth:`from_unsigned` or :meth:`from_signed`. .. code-block:: python3 >>> LogicArray.from_unsigned(0xA) # picks smallest range that can fit the value LogicArray('1010', Range(3, 'downto', 0)) >>> LogicArray.from_signed(-4, Range(0, "to", 3)) # will sign-extend LogicArray('1100', Range(0, 'to', 3)) :class:`LogicArray`\ s support the same operations as :class:`Array`; however, it enforces the condition that all elements must be a :class:`Logic`. .. code-block:: python3 >>> la = LogicArray("1010") >>> la[0] # is indexable Logic('0') >>> la[1:] # is slice-able LogicArray('10', Range(1, 'downto', 0)) >>> Logic("0") in la # is a collection True >>> list(la) # is an iterable [Logic('1'), Logic('0'), Logic('1'), Logic('0')] When setting an element or slice, the *value* is first constructed into a :class:`Logic`. .. code-block:: python3 >>> la = LogicArray("1010") >>> la[3] = "Z" >>> la[3] Logic('Z') >>> la[2:] = ['X', True, 0] >>> la LogicArray('ZX10', Range(3, 'downto', 0)) :class:`LogicArray`\ s can be converted into :class:`str`\ s or :class:`int`\ s. .. code-block:: python3 >>> la = LogicArray("1010") >>> str(la) '1010' >>> la.to_unsigned() 10 >>> la.to_signed() -6 :class:`LogicArray`\ s also support element-wise logical operations: ``&``, ``|``, ``^``, and ``~``. .. code-block:: python3 >>> def big_mux(a: LogicArray, b: LogicArray, sel: Logic) -> LogicArray: ... s = LogicArray([sel] * len(a)) ... return (a & ~s) | (b & s) >>> la = LogicArray("0110") >>> p = LogicArray("1110") >>> sel = Logic('1') # choose second option >>> big_mux(la, p, sel) LogicArray('1110', Range(3, 'downto', 0)) Args: value: Initial value for the array. range: Indexing scheme of the array. Raises: OverflowError: When given *value* cannot fit in given *range*. ValueError: When argument values cannot be used to construct an array. TypeError: When invalid argument types are used. """ _value: typing.List[Logic] _range: Range @typing.overload def __new__( cls, value: typing.Union[int, typing.Iterable[LogicConstructibleT]], range: typing.Optional[Range] = None, ) -> "LogicArray": ... @typing.overload def __new__( cls, value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None, *, range: Range, ) -> "LogicArray": ... def __new__( cls, value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None, range: typing.Optional[Range] = None, ) -> "LogicArray": if isinstance(value, int): warnings.warn( "Constructing a LogicArray from an integer is deprecated. " "Use `LogicArray.from_signed(value)` or `LogicArray.from_unsigned(value)` instead.", DeprecationWarning, stacklevel=2, ) if value < 0: return cls.from_signed(value, range=range) else: return cls.from_unsigned(value, range=range) self = super().__new__(cls) # construct _value representation if value is None: if range is None: raise ValueError( "at least one of the value and range input parameters must be given" ) self._value = [Logic() for _ in range] else: value_iter = iter(value) self._value = [Logic(v) for v in value_iter] # construct _range representation if range is None: self._range = Range(len(self._value) - 1, "downto", 0) else: self._range = range # check that _value and _range align if len(self._value) != len(self._range): raise OverflowError( f"value of length {len(self._value)} will not fit in {self._range}" ) return self
[docs] @classmethod def from_unsigned( cls, value: int, range: typing.Optional[Range] = None ) -> "LogicArray": """Construct a :class:`LogicArray` from an :class:`int` by interpreting it as a bit vector with unsigned representation. The :class:`int` is treated as an arbitrary-length bit vector with unsigned representation where the left-most bit is the most significant bit. This bit vector is then constructed into a :class:`LogicArray`. If *range* is not given, it defaults to ``Range(n_bits-1, "downto", 0)``, where ``n_bits`` is the minimum number of bits necessary to hold the value. If *range* is given and the value cannot fit in a :class:`LogicArray` of that size, an :exc:`OverflowError` is raised. Args: value: The integer to convert. range: A specific :class:`Range` to use as the bounds on the return :class:`LogicArray` object. Returns: A :class:`LogicArray` equivalent to the *value* by interpreting it as a bit vector with unsigned representation. Raises: OverflowError: When a :class:`LogicArray` of the given *range* can't hold the *value*. """ if value < 0: raise OverflowError(f"{value} not in bounds for an unsigned integer.") bitlen = max(1, int.bit_length(value)) if range is None: range = Range(bitlen - 1, "downto", 0) elif bitlen > len(range): raise OverflowError( f"{value} will not fit in a LogicArray with bounds: {range!r}." ) return LogicArray(_int_to_bitstr(value, len(range)), range=range)
[docs] @classmethod def from_signed( cls, value: int, range: typing.Optional[Range] = None ) -> "LogicArray": """Construct a :class:`LogicArray` from an :class:`int` by interpreting it as a bit vector with two's complement representation. The :class:`int` is treated as an arbitrary-length bit vector with two's complement representation where the left-most bit is the most significant bit. This bit vector is then constructed into a :class:`LogicArray`. If *range* is not given, it defaults to ``Range(n_bits-1, "downto", 0)``, where ``n_bits`` is the minimum number of bits necessary to hold the value. If *range* is given and the value cannot fit in a :class:`LogicArray` of that size, an :exc:`OverflowError` is raised. Args: value: The integer to convert. range: A specific :class:`Range` to use as the bounds on the return :class:`LogicArray` object. Returns: A :class:`LogicArray` equivalent to the *value* by interpreting it as a bit vector with two's complement representation. Raises: OverflowError: When a :class:`LogicArray` of the given *range* can't hold the *value*. """ bitlen = int.bit_length(value + 1) + 1 if range is None: range = Range(bitlen - 1, "downto", 0) elif bitlen > len(range): raise OverflowError( f"{value} will not fit in a LogicArray with bounds: {range!r}." ) return LogicArray(_int_to_bitstr(value, len(range)), range=range)
@property def range(self) -> Range: """:class:`Range` of the indexes of the array.""" return self._range @range.setter def range(self, new_range: Range) -> None: """Set a new indexing scheme on the array. Must be the same size.""" if not isinstance(new_range, Range): raise TypeError("range argument must be of type 'Range'") if len(new_range) != len(self): raise ValueError( f"{new_range!r} not the same length as old range: {self._range!r}." ) self._range = new_range def __iter__(self) -> typing.Iterator[Logic]: return iter(self._value) def __reversed__(self) -> typing.Iterator[Logic]: return reversed(self._value) def __contains__(self, item: object) -> bool: return item in self._value def __eq__( self, other: object, ) -> bool: if isinstance(other, LogicArray): return self._value == other._value elif isinstance(other, int): try: return self.to_unsigned() == other except ValueError: return False elif isinstance(other, (str, list, tuple)): try: other = LogicArray(other) except ValueError: return False return self == other else: return NotImplemented
[docs] def count(self, value: Logic) -> int: """Return number of occurrences of *value*.""" return self._value.count(value)
@property @deprecated("`.binstr` property is deprecated. Use `str(value)` instead.") def binstr(self) -> str: """Convert the value to the :class:`str` literal representation. .. deprecated:: 2.0 """ return str(self) @property def is_resolvable(self) -> bool: """``True`` if all elements are ``0`` or ``1``.""" return all(bit in (Logic(0), Logic(1)) for bit in self) @property @deprecated("`.integer` property is deprecated. Use `value.to_unsigned()` instead.") def integer(self) -> int: """Convert the value to an :class:`int` by interpreting it using unsigned representation. The :class:`LogicArray` is treated as an arbitrary-length vector of bits with the left-most bit being the most significant bit in the integer value. The bit vector is then interpreted as an integer using unsigned representation. Returns: An :class:`int` equivalent to the value by interpreting it using unsigned representation. .. deprecated:: 2.0 """ return self.to_unsigned() @property @deprecated( "`.signed_integer` property is deprecated. Use `value.to_signed()` instead." ) def signed_integer(self) -> int: """Convert the value to an :class:`int` by interpreting it using two's complement representation. The :class:`LogicArray` is treated as an arbitrary-length vector of bits with the left-most bit being the most significant bit in the integer value. The bit vector is then interpreted as an integer using two's complement representation. Returns: An :class:`int` equivalent to the value by interpreting it using two's complement representation. .. deprecated:: 2.0 """ return self.to_signed()
[docs] def to_unsigned(self) -> int: """Convert the value to an :class:`int` by interpreting it using unsigned representation. The :class:`LogicArray` is treated as an arbitrary-length vector of bits with the left-most bit being the most significant bit in the integer value. The bit vector is then interpreted as an integer using unsigned representation. Returns: An :class:`int` equivalent to the value by interpreting it using unsigned representation. """ value = 0 for bit in self: value = value << 1 | int(bit) return value
[docs] def to_signed(self) -> int: """Convert the value to an :class:`int` by interpreting it using two's complement representation. The :class:`LogicArray` is treated as an arbitrary-length vector of bits with the left-most bit being the most significant bit in the integer value. The bit vector is then interpreted as an integer using two's complement representation. Returns: An :class:`int` equivalent to the value by interpreting it using two's complement representation. """ value = self.to_unsigned() if value >= (1 << (len(self) - 1)): value -= 1 << len(self) return value
@typing.overload def __getitem__(self, item: int) -> Logic: ... @typing.overload def __getitem__(self, item: slice) -> "LogicArray": ... def __getitem__( self, item: typing.Union[int, slice] ) -> typing.Union[Logic, "LogicArray"]: if isinstance(item, int): idx = self._translate_index(item) return self._value[idx] elif isinstance(item, slice): start = item.start if item.start is not None else self.left stop = item.stop if item.stop is not None else self.right if item.step is not None: raise IndexError("do not specify step") start_i = self._translate_index(start) stop_i = self._translate_index(stop) if start_i > stop_i: raise IndexError( f"slice [{start}:{stop}] direction does not match array direction [{self.left}:{self.right}]" ) value = self._value[start_i : stop_i + 1] range = Range(start, self.direction, stop) return LogicArray(value=value, range=range) raise TypeError(f"indexes must be ints or slices, not {type(item).__name__}") @typing.overload def __setitem__(self, item: int, value: LogicConstructibleT) -> None: ... @typing.overload def __setitem__( self, item: slice, value: typing.Iterable[LogicConstructibleT] ) -> None: ... def __setitem__( self, item: typing.Union[int, slice], value: typing.Union[LogicConstructibleT, typing.Iterable[LogicConstructibleT]], ) -> None: if isinstance(item, int): idx = self._translate_index(item) self._value[idx] = Logic(typing.cast(LogicConstructibleT, value)) elif isinstance(item, slice): start = item.start if item.start is not None else self.left stop = item.stop if item.stop is not None else self.right if item.step is not None: raise IndexError("do not specify step") start_i = self._translate_index(start) stop_i = self._translate_index(stop) if start_i > stop_i: raise IndexError( f"slice [{start}:{stop}] direction does not match array direction [{self.left}:{self.right}]" ) value_as_logics = [ Logic(v) for v in typing.cast(typing.Iterable[LogicConstructibleT], value) ] if len(value_as_logics) != (stop_i - start_i + 1): raise ValueError( f"value of length {len(value_as_logics)!r} will not fit in slice [{start}:{stop}]" ) self._value[start_i : stop_i + 1] = value_as_logics else: raise TypeError( f"indexes must be ints or slices, not {type(item).__name__}" ) def _translate_index(self, item: int) -> int: try: return self._range.index(item) except ValueError: raise IndexError(f"index {item} out of range") from None def __repr__(self) -> str: return f"{type(self).__qualname__}({str(self)!r}, {self.range!r})" def __str__(self) -> str: return "".join(str(bit) for bit in self) def __int__(self) -> int: return self.to_unsigned() def __and__(self, other: "LogicArray") -> "LogicArray": if isinstance(other, LogicArray): if len(self) != len(other): raise ValueError( f"cannot perform bitwise & " f"between {type(self).__qualname__} of length {len(self)} " f"and {type(other).__qualname__} of length {len(other)}" ) return LogicArray(a & b for a, b in zip(self, other)) return NotImplemented def __or__(self, other: "LogicArray") -> "LogicArray": if isinstance(other, LogicArray): if len(self) != len(other): raise ValueError( f"cannot perform bitwise | " f"between {type(self).__qualname__} of length {len(self)} " f"and {type(other).__qualname__} of length {len(other)}" ) return LogicArray(a | b for a, b in zip(self, other)) return NotImplemented def __xor__(self, other: "LogicArray") -> "LogicArray": if isinstance(other, LogicArray): if len(self) != len(other): raise ValueError( f"cannot perform bitwise ^ " f"between {type(self).__qualname__} of length {len(self)} " f"and {type(other).__qualname__} of length {len(other)}" ) return LogicArray(a ^ b for a, b in zip(self, other)) return NotImplemented def __invert__(self) -> "LogicArray": return LogicArray(~v for v in self)
def _int_to_bitstr(value: int, n_bits: int) -> str: if value < 0: value += 1 << n_bits return format(value, f"0{n_bits}b")