Skip to content

Commit

Permalink
Add Logic and Bit datatypes (#2485)
Browse files Browse the repository at this point in the history
* Add Logic and Bit datatypes

Logic (4-value) and Bit (2-value) datatypes models are added for use in
system modelling and eventual use in the handle class.

* Update cocotb/_py_compat.py

* Add license headers

* One sentence per line

* Reword docstring

* Add versionadded directive to documentation

* Move versionadded directive directly after summary paragraph

* Update cocotb/types/logic.py

Co-authored-by: Colin Marquardt <cmarqu42@gmail.com>

* Apply suggestions from code review

Co-authored-by: Colin Marquardt <cmarqu42@gmail.com>

* Add tests for copy-conversions

* Add hashability test

* Improve tests of invalid values

* Add self-identity tests

* Update cocotb/types/logic.py

Co-authored-by: Marlon James <marlon.james@gmail.com>

* Apply suggestions from code review

Whoops deleted "don't care" again.

Co-authored-by: Marlon James <marlon.james@gmail.com>

* Update cocotb/types/logic.py

* Apply suggestions from code review

* Apply suggestions from code review

* Update cocotb/types/logic.py

Co-authored-by: Eric Wieser <wieser.eric@gmail.com>

* Update cocotb/types/logic.py

Co-authored-by: Colin Marquardt <cmarqu42@gmail.com>

* Expand Logic and Bit docstring

* Add test for logic and bit not being identical

* Simplify

* Apply suggestions from code review

Co-authored-by: Colin Marquardt <cmarqu42@gmail.com>

* Apply suggestions from code review

Co-authored-by: Marlon James <marlon.james@gmail.com>

* Formatting with black

* Add doctests

* Add __slots__

* Add type annotations

* Make Bit a proper subtype

This requires implementing __eq__. The performance cost of __eq__ means
that caching is no longer an effective performance boost. Without
identity equality, __singleton_cache__ is pointless, so it was removed.
This simplified the implementation and made Bit a proper subtype of
Logic.

* .__class__ is faster on average for equivalent operation

* Use __qualname__ in __repr__

* Improve Logic and Bit docstrings

* Fix type annotations

Logic only supports operations with other Logic types and subtypes, so
lets make that clear.

* Use type() instead of obj.__class__

* Apply suggestions from code review

Co-authored-by: Eric Wieser <wieser.eric@gmail.com>

* Update cocotb/types/logic.py

* Fix flake8

* Reintroduce singleton cache and indentity equality

* Add static attributes to reference values

Co-authored-by: Colin Marquardt <cmarqu42@gmail.com>
Co-authored-by: Marlon James <marlon.james@gmail.com>
Co-authored-by: Eric Wieser <wieser.eric@gmail.com>
  • Loading branch information
4 people committed Jun 9, 2021
1 parent 1cfe6cb commit c065ccb
Show file tree
Hide file tree
Showing 6 changed files with 615 additions and 0 deletions.
4 changes: 4 additions & 0 deletions cocotb/types/__init__.py
@@ -0,0 +1,4 @@
# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
from .logic import Logic, Bit # noqa: F401
325 changes: 325 additions & 0 deletions cocotb/types/logic.py
@@ -0,0 +1,325 @@
# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
from typing import Any, Optional, Dict
from typing import Tuple # noqa: F401
from functools import lru_cache


class _StaticOnlyProp:

def __init__(self, fget):
self.fget = fget

def __set_name__(self, cls, name):
self.__cls = cls

def __get__(self, instance, cls):
if cls is not self.__cls or instance is not None:
raise AttributeError
return self.fget()


class Logic:
r"""
Model of a 4-value (``0``, ``1``, ``X``, ``Z``) datatype commonly seen in HDLs.
.. currentmodule:: cocotb.types
This is modeled after (System)Verilog's 4-value ``logic`` type.
VHDL's 9-value ``std_ulogic`` type maps to this type by treating weak values as full strength values
and treating "uninitialized" (``U``) and "don't care" (``-``) as "unknown" (``X``).
:class:`Logic` can be converted to and from :class:`int`, :class:`str`, :class:`bool`, and :class:`Bit`
by using the appropriate constructor syntax.
The list of values convertable to :class:`Logic` includes
``0``, ``1``, ``True``, ``False``, ``"0"``, ``"1"``, ``"X"``, ``"Z"``, ``Bit('0')``, and ``Bit('1')``.
For a comprehensive list of values that can be converted into :class:`Logic` see :file:`tests/pytest/test_logic.py`.
.. code-block:: python3
>>> Logic("X")
Logic('X')
>>> Logic(True)
Logic('1')
>>> Logic(1)
Logic('1')
>>> Logic(Bit(0))
Logic('0')
>>> Logic() # default value
Logic('X')
>>> str(Logic("Z"))
'Z'
>>> bool(Logic(0))
False
>>> int(Logic(1))
1
>>> Bit(Logic("1"))
Bit('1')
.. note::
The :class:`int` and :class:`bool` conversions will raise :exc:`ValueError` if the value is not ``0`` or ``1``.
:class:`Logic` values are immutable and therefore hashable and can be placed in :class:`set`\ s and used as keys in :class:`dict`\ s.
:class:`Logic` supports the common logic operations ``&``, ``|``, ``^``, and ``~``.
.. code-block:: python3
>>> def full_adder(a: Logic, b: Logic, carry: Logic) -> Tuple[Logic, Logic]:
... res = a ^ b ^ carry
... carry_out = (a & b) | (b & carry) | (a & carry)
... return res, carry_out
>>> full_adder(a=Logic('0'), b=Logic('1'), carry=Logic('1'))
(Logic('0'), Logic('1'))
Args:
value: value to construct into a :class:`Logic`.
Raises:
ValueError: if the value cannot be constructed into a :class:`Logic`.
"""
__slots__ = ("_repr",)

__singleton_cache__: Dict[int, "Logic"] = {}

_repr_map = {
# 0 and weak 0
False: 0,
0: 0,
"0": 0,
"L": 0,
"l": 0,
# 1 and weak 1
True: 1,
1: 1,
"1": 1,
"H": 1,
"h": 1,
# unknown, unassigned, and weak unknown
None: 2,
"X": 2,
"x": 2,
"U": 2,
"u": 2,
"W": 2,
"w": 2,
"-": 2,
# high impedance
"Z": 3,
"z": 3,
}

@_StaticOnlyProp
def _0():
return Logic("0")

@_StaticOnlyProp
def _1():
return Logic("1")

@_StaticOnlyProp
def X():
return Logic("X")

@_StaticOnlyProp
def Z():
return Logic("Z")

@lru_cache(maxsize=None)
def __new__(cls, value: Optional[Any] = None) -> "Logic":
# convert to internal representation
try:
_repr = cls._repr_map[value]
except KeyError:
raise ValueError(
"{!r} is not convertible to a {}".format(value, cls.__qualname__)
) from None
obj = cls.__singleton_cache__.get(_repr, None)
if obj is None:
obj = super().__new__(cls)
obj._repr = _repr
cls.__singleton_cache__[_repr] = obj
return obj

def __and__(self, other: "Logic") -> "Logic":
if not isinstance(other, type(self)):
return NotImplemented
return type(self)(
(
("0", "0", "0", "0"),
("0", "1", "X", "X"),
("0", "X", "X", "X"),
("0", "X", "X", "X"),
)[self._repr][other._repr]
)

def __rand__(self, other: "Logic") -> "Logic":
return self & other

def __or__(self, other: "Logic") -> "Logic":
if not isinstance(other, type(self)):
return NotImplemented
return type(self)(
(
("0", "1", "X", "X"),
("1", "1", "1", "1"),
("X", "1", "X", "X"),
("X", "1", "X", "X"),
)[self._repr][other._repr]
)

def __ror__(self, other: "Logic") -> "Logic":
return self | other

def __xor__(self, other: "Logic") -> "Logic":
if not isinstance(other, type(self)):
return NotImplemented
return type(self)(
(
("0", "1", "X", "X"),
("1", "0", "X", "X"),
("X", "X", "X", "X"),
("X", "X", "X", "X"),
)[self._repr][other._repr]
)

def __rxor__(self, other: "Logic") -> "Logic":
return self ^ other

def __invert__(self) -> "Logic":
return type(self)(("1", "0", "X", "X")[self._repr])

def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return self._repr == other._repr

def __hash__(self) -> int:
return self._repr

def __repr__(self) -> str:
return "{}({!r})".format(type(self).__qualname__, str(self))

def __str__(self) -> str:
return ("0", "1", "X", "Z")[self._repr]

def __bool__(self) -> bool:
if self._repr < 2:
return bool(self._repr)
raise ValueError(f"Cannot convert {self!r} to bool")

def __int__(self) -> int:
if self._repr < 2:
return self._repr
raise ValueError(f"Cannot convert {self!r} to int")


class Bit(Logic):
r"""
Model of a 2-value (``0``, ``1``) datatype commonly seen in HDLs.
.. currentmodule:: cocotb.types
This is modeled after (System)Verilog's 2-value ``bit`` type.
VHDL's ``bit`` type maps to this type perfectly.
:class:`Bit` is a proper subtype of :class:`Logic`, meaning a use of :class:`Logic` can be substituted with a :class:`Bit`.
Some behavior may surprise you if you do not expect it.
.. code-block:: python3
>>> Bit(0) == Logic(0)
True
>>> Bit(0) in {Logic(0)}
True
:class:`Bit` can be converted to and from :class:`int`, :class:`str`, :class:`bool`, and :class:`Logic`
by using the appropriate constructor syntax.
The list of values convertable to :class:`Bit` includes
``0``, ``1``, ``True``, ``False``, ``"0"``, ``"1"``, ``Logic('0')``, and ``Logic('1')``.
For a comprehensive list of values that can be converted into :class:`Bit` see :file:`tests/pytest/test_logic.py`.
.. code-block:: python3
>>> Bit("0")
Bit('0')
>>> Bit(True)
Bit('1')
>>> Bit(1)
Bit('1')
>>> Bit(Logic(0))
Bit('0')
>>> Bit() # default value
Bit('0')
>>> str(Bit("0"))
'0'
>>> bool(Bit(False))
False
>>> int(Bit(1))
1
>>> Logic(Bit("1"))
Logic('1')
:class:`Bit` values are hashable and can be placed in :class:`set`\ s and used as keys in :class:`dict`\ s.
:class:`Bit` supports the common logic operations ``&``, ``|``, ``^``, and ``~``.
.. code-block:: py3
>>> def mux(a: Bit, b: Bit, s: Bit) -> Bit:
... return (a & ~s) | (b & s)
>>> a = Bit(0)
>>> b = Bit(1)
>>> sel = Bit(1) # choose second argument
>>> mux(a, b, sel)
Bit('1')
Args:
value: value to construct into a :class:`Bit`.
Raises:
ValueError: if the value cannot be constructed into a :class:`Bit`.
"""
__slots__ = ()

__singleton_cache__: Dict[int, "Bit"] = {}

_repr_map = {
# 0
None: 0,
False: 0,
0: 0,
"0": 0,
# 1
True: 1,
1: 1,
"1": 1,
}

@_StaticOnlyProp
def _0():
return Bit("0")

@_StaticOnlyProp
def _1():
return Bit("1")


Logic._repr_map.update(
{
Logic("0"): 0,
Logic("1"): 1,
Logic("X"): 2,
Logic("Z"): 3,
}
)

Bit._repr_map.update({Bit("0"): 0, Bit("1"): 1})
14 changes: 14 additions & 0 deletions documentation/source/library_reference.rst
Expand Up @@ -62,6 +62,20 @@ Interacting with the Simulator

.. autofunction:: cocotb.decorators.RunningTask.kill

HDL Datatypes
-------------

These are a set of datatypes that model the behavior of common HDL datatypes.
They can be used independently of cocotb for modeling and will replace :class:`BinaryValue`
as the types used by cocotb's `simulator handles <#simulation-object-handles>`_.

.. versionadded:: 2.0

.. autoclass:: cocotb.types.Logic

.. autoclass:: cocotb.types.Bit


Triggers
--------
See :ref:`simulator-triggers` for a list of sub-classes. Below are the internal
Expand Down
1 change: 1 addition & 0 deletions documentation/source/newsfragments/2059.feature.rst
@@ -0,0 +1 @@
Added :class:`~cocotb.types.Logic` and :class:`~cocotb.types.Bit` modeling datatypes.
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -33,6 +33,7 @@ testpaths =
tests/pytest
cocotb/utils.py
cocotb/binary.py
cocotb/types/
cocotb/_sim_versions.py
# log_cli = true
# log_cli_level = DEBUG
Expand Down

0 comments on commit c065ccb

Please sign in to comment.