# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
import typing
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`;
or from integers.
If constructed from a positive integer, an unsigned bit representation is used to
construct the :class:`LogicArray`.
If constructed from a negative integer, a two's complement bit representation is
used.
Like :class:`Array`, if no *range* argument is given, it is deduced from the length
of the iterable or bit string 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(0xA) # picks smallest range that can fit the value
LogicArray('1010', Range(3, 'downto', 0))
>>> LogicArray(-4, Range(0, "to", 3)) # will sign-extend
LogicArray('1100', Range(0, 'to', 3))
>>> LogicArray(range=Range(0, "to", 3)) # default values
LogicArray('XXXX', 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")
>>> la.binstr
'1010'
>>> la.integer # uses unsigned representation
10
>>> la.signed_integer # uses two's complement representation
-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:
ValueError: When argument values cannot be used to construct an array.
TypeError: When invalid argument types are used.
"""
__slots__ = ()
@typing.overload
def __init__(
self,
value: typing.Union[int, typing.Iterable[LogicConstructibleT]],
range: typing.Optional[Range] = None,
): ...
@typing.overload
def __init__(
self,
value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None,
*,
range: Range,
): ...
def __init__(
self,
value: typing.Union[int, typing.Iterable[LogicConstructibleT], None] = None,
range: typing.Optional[Range] = None,
) -> None:
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]
elif isinstance(value, int):
if value < 0:
bitlen = int.bit_length(value + 1) + 1
else:
bitlen = max(1, int.bit_length(value))
if range is None:
self._value = [Logic(v) for v in _int_to_bitstr(value, bitlen)]
else:
if bitlen > len(range):
raise ValueError(f"{value} will not fit in {range}")
self._value = [Logic(v) for v in _int_to_bitstr(value, len(range))]
elif isinstance(value, typing.Iterable):
self._value = [Logic(v) for v in value]
else:
raise TypeError(
f"cannot construct {type(self).__qualname__} from value of type {type(value).__qualname__}"
)
if range is None:
self._range = Range(len(self._value) - 1, "downto", 0)
else:
self._range = range
if len(self._value) != len(self._range):
raise ValueError(
f"value of length {len(self._value)} will not fit in {self._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.integer == 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
def binstr(self) -> str:
return "".join(str(bit) for bit in self)
@property
def is_resolvable(self) -> bool:
return all(bit in (Logic(0), Logic(1)) for bit in self)
@property
def integer(self) -> int:
value = 0
for bit in self:
value = value << 1 | int(bit)
return value
@property
def signed_integer(self) -> int:
value = self.integer
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__}({self.binstr!r}, {self.range!r})"
def __str__(self) -> str:
return self.binstr
def __int__(self) -> int:
return self.integer
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")