# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
import typing
T = typing.TypeVar("T")
[docs]
class Range(typing.Sequence[int]):
r"""
Variant of :class:`range` with inclusive right bound.
In Python, :class:`range` and :class:`slice` have a non-inclusive right bound.
In both Verilog and VHDL, ranges and arrays have an inclusive right bound.
This type mimics Python's :class:`range` type, but implements HDL-like inclusive right bounds,
using the names :attr:`left` and :attr:`right` as replacements for ``start`` and ``stop`` to
match VHDL.
Range directionality can be specified using ``'to'`` or ``'downto'`` between the
left and right bounds.
Not specifying directionality will cause the directionality to be inferred.
.. code-block:: python3
>>> r = Range(-2, 3)
>>> r.left, r.right, len(r)
(-2, 3, 6)
>>> s = Range(8, 'downto', 1)
>>> s.left, s.right, len(s)
(8, 1, 8)
:meth:`from_range` and :meth:`to_range` can be used to convert from and to :class:`range`.
.. code-block:: python3
>>> r = Range(-2, 3)
>>> r.to_range()
range(-2, 4)
:class:`Range` supports "null" ranges as seen in VHDL.
"null" ranges occur when a left bound cannot reach a right bound with the given direction.
They have a length of 0, but the :attr:`left`, :attr:`right`, and :attr:`direction` values remain as given.
.. code-block:: python3
>>> r = Range(1, 'to', 0) # no way to count from 1 'to' 0
>>> r.left, r.direction, r.right
(1, 'to', 0)
>>> len(r)
0
.. note::
This is only possible when specifying the direction.
Ranges also support all the features of :class:`range` including, but not limited to:
- ``value in range`` to see if a value is in the range,
- ``range.index(value)`` to see what position in the range the value is,
The typical use case of this type is in conjunction with :class:`~cocotb.types.Array`.
Args:
left: leftmost bound of range
direction: ``'to'`` if values are ascending, ``'downto'`` if descending
right: rightmost bound of range (inclusive)
"""
__slots__ = ("_range",)
@typing.overload
def __init__(self, left: int, direction: int) -> None:
pass # pragma: no cover
@typing.overload
def __init__(self, left: int, direction: str, right: int) -> None:
pass # pragma: no cover
@typing.overload
def __init__(self, left: int, *, right: int) -> None:
pass # pragma: no cover
def __init__(
self,
left: int,
direction: typing.Union[int, str, None] = None,
right: typing.Union[int, None] = None,
) -> None:
start = left
stop: int
step: int
if isinstance(direction, int) and right is None:
step = _guess_step(left, direction)
stop = direction + step
elif direction is None and isinstance(right, int):
step = _guess_step(left, right)
stop = right + step
elif isinstance(direction, str) and isinstance(right, int):
step = _direction_to_step(direction)
stop = right + step
else:
raise TypeError("invalid arguments")
self._range = range(start, stop, step)
[docs]
@classmethod
def from_range(cls, range: range) -> "Range":
"""Convert :class:`range` to :class:`Range`."""
return cls(
left=range.start,
direction=_step_to_direction(range.step),
right=(range.stop - range.step),
)
[docs]
def to_range(self) -> range:
"""Convert :class:`Range` to :class:`range`."""
return self._range
@property
def left(self) -> int:
"""Leftmost value in a range."""
return self._range.start
@property
def direction(self) -> str:
"""``'to'`` if values are meant to be ascending, ``'downto'`` otherwise."""
return _step_to_direction(self._range.step)
@property
def right(self) -> int:
"""Rightmost value in a range."""
return self._range.stop - self._range.step
def __len__(self) -> int:
return len(self._range)
@typing.overload
def __getitem__(self, item: int) -> int:
pass # pragma: no cover
@typing.overload
def __getitem__(self, item: slice) -> "Range":
pass # pragma: no cover
def __getitem__(self, item: typing.Union[int, slice]) -> typing.Union[int, "Range"]:
if isinstance(item, int):
return self._range[item]
elif isinstance(item, slice):
return type(self).from_range(self._range[item])
raise TypeError(
"indices must be integers or slices, not {}".format(type(item).__name__)
)
def __contains__(self, item: object) -> bool:
return item in self._range
def __iter__(self) -> typing.Iterator[int]:
return iter(self._range)
def __reversed__(self) -> typing.Iterator[int]:
return reversed(self._range)
def __eq__(self, other: object) -> bool:
if isinstance(other, type(self)):
return self._range == other._range
return NotImplemented # must not be in a type narrowing context to be ignored properly
def __hash__(self) -> int:
return hash(self._range)
def count(self, item: int) -> int:
return self._range.count(item)
def __repr__(self) -> str:
return "{}({!r}, {!r}, {!r})".format(
type(self).__qualname__, self.left, self.direction, self.right
)
def _guess_step(left: int, right: int) -> int:
if left <= right:
return 1
return -1
def _direction_to_step(direction: str) -> int:
direction = direction.lower()
if direction == "to":
return 1
elif direction == "downto":
return -1
raise ValueError("direction must be 'to' or 'downto'")
def _step_to_direction(step: int) -> str:
if step == 1:
return "to"
elif step == -1:
return "downto"
raise ValueError("step must be 1 or -1")