Repository: cocotb/cocotb

Issue:
Consider changing base class of NonConstantObject
I don't see why `NonConstantObject` should inherit from `NonHierarchyIndexableObject`. Wouldn't it make more sense for it to be another branch from `NonHierarchyObject`?

See https://cocotb.readthedocs.io/en/latest/library_reference.html#simulation-object-handles for class diagram.




Discussion:
It's worth thinking about the effect this would have on these lines:

https://github.com/cocotb/cocotb/blob/7c3f44df806184d1b3ccade7b8492bde826dd34e/cocotb/handle.py#L810-L821

Not to say that I've done that thinking yet.
Has your question been resolved? If so please close this issue. If it has not been resolved, you may need to provide more information. If no more activity on this issue occurs in 7 days, it will be closed.

Resolving patch:
diff --git a/src/cocotb/handle.py b/src/cocotb/handle.py
--- a/src/cocotb/handle.py
+++ b/src/cocotb/handle.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-
 # Copyright (c) 2013 Potential Ventures Ltd
 # Copyright (c) 2013 SolarFlare Communications Inc
 # All rights reserved.
@@ -27,14 +25,15 @@
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-# -*- coding: utf-8 -*-
-
 import enum
+from abc import ABC, abstractmethod
 from functools import lru_cache
-from typing import Optional
+from logging import Logger
+from typing import Dict, Generic, Set, Tuple, TypeVar
 
 import cocotb
 from cocotb import simulator
+from cocotb._py_compat import cached_property
 from cocotb.binary import BinaryRepresentation, BinaryValue
 from cocotb.log import SimLog
 from cocotb.types import Logic, LogicArray
@@ -62,12 +61,13 @@ def _value_limits(n_bits, limits):
     return min_val, max_val
 
 
-class SimHandleBase:
+class SimHandleBase(ABC):
     """Base class for all simulation objects.
 
     We maintain a handle which we can use for GPI calls.
     """
 
+    @abstractmethod
     def __init__(self, handle, path):
         """
         .. Constructor. This RST comment works around sphinx-doc/sphinx#6885
@@ -77,33 +77,40 @@ def __init__(self, handle, path):
             path (str): Path to this handle, ``None`` if root.
         """
         self._handle = handle
-        self._len: Optional[int] = None
-        """The "length" (the number of elements) of the underlying object. For vectors this is the number of bits."""
-        self._sub_handles: dict = {}
-        """Dictionary of this handle's children."""
-        self._invalid_sub_handles: set = set()
-        """Python :class:`set` of invalid queries, for caching purposes."""
-        self._name: str = self._handle.get_name_string()
+        self._path: str = self._name if path is None else path
+        """The path to this handle, or its name if this is the root handle.
+
+        :meta public:
+        """
+
+    @cached_property
+    def _name(self) -> str:
         """The name of an object.
 
         :meta public:
         """
-        self._type: str = self._handle.get_type_string()
+        return self._handle.get_name_string()
+
+    @cached_property
+    def _type(self) -> str:
         """The type of an object as a string.
 
         :meta public:
         """
-        self._fullname: str = self._name + "(%s)" % self._type
+        return self._handle.get_type_string()
+
+    @cached_property
+    def _fullname(self) -> str:
         """The name of an object with its type appended in parentheses."""
-        self._path: str = self._name if path is None else path
-        """The path to this handle, or its name if this is the root handle.
+        return self._name + "(%s)" % self._type
 
-        :meta public:
-        """
-        self._log = SimLog("cocotb.%s" % self._name)
+    @cached_property
+    def _log(self) -> Logger:
         """The logging object."""
-        self._log.debug("Created")
-        self._def_name: str = self._handle.get_definition_name()
+        return SimLog("cocotb.%s" % self._name)
+
+    @cached_property
+    def _def_name(self) -> str:
         """The name of a GPI object's definition.
 
         This is the value of ``vpiDefName`` for VPI, ``vhpiNameP`` for VHPI,
@@ -112,7 +119,10 @@ def __init__(self, handle, path):
 
         :meta public:
         """
-        self._def_file: str = self._handle.get_definition_file()
+        return self._handle.get_definition_name()
+
+    @cached_property
+    def _def_file(self) -> str:
         """The name of the file that sources the object's definition.
 
         This is the value of ``vpiDefFile`` for VPI, ``vhpiFileNameP`` for VHPI,
@@ -121,6 +131,7 @@ def __init__(self, handle, path):
 
         :meta public:
         """
+        return self._handle.get_definition_file()
 
     def get_definition_name(self):
         return self._def_name
@@ -131,15 +142,6 @@ def get_definition_file(self):
     def __hash__(self):
         return hash(self._handle)
 
-    def __len__(self):
-        """Return the "length" (the number of elements) of the underlying object.
-
-        For vectors this is the number of bits.
-        """
-        if self._len is None:
-            self._len = self._handle.get_num_elems()
-        return self._len
-
     def __eq__(self, other):
         """Compare equality of handles.
 
@@ -152,11 +154,6 @@ def __eq__(self, other):
             return NotImplemented
         return self._handle == other._handle
 
-    def __ne__(self, other):
-        if not isinstance(other, SimHandleBase):
-            return NotImplemented
-        return self._handle != other._handle
-
     def __repr__(self):
         desc = self._path
         defname = self._def_name
@@ -167,56 +164,35 @@ def __repr__(self):
                 desc += " (at " + deffile + ")"
         return type(self).__qualname__ + "(" + desc + ")"
 
-    def __str__(self):
-        return self._path
+
+IndexType = TypeVar("IndexType")
 
 
-class RegionObject(SimHandleBase):
+class RegionObject(SimHandleBase, Generic[IndexType]):
     """A region object, such as a scope or namespace.
 
     Region objects don't have values, they are effectively scopes or namespaces.
     """
 
+    @abstractmethod
     def __init__(self, handle, path):
-        SimHandleBase.__init__(self, handle, path)
-        self._discovered = False  # True if this object has already been discovered
+        super().__init__(handle, path)
+        self._sub_handles: Dict[IndexType, SimHandleBase] = {}
 
     def __iter__(self):
         """Iterate over all known objects in this layer of hierarchy."""
-        if not self._discovered:
-            self._discover_all()
+        self._discover_all()
 
         for name, handle in self._sub_handles.items():
-            if isinstance(handle, list):
-                self._log.debug("Found index list length %d", len(handle))
-                for subindex, subhdl in enumerate(handle):
-                    if subhdl is None:
-                        self._log.warning(
-                            "Index %d doesn't exist in %s.%s",
-                            subindex,
-                            self._name,
-                            name,
-                        )
-                        continue
-                    self._log.debug(
-                        "Yielding index %d from %s (%s)", subindex, name, type(subhdl)
-                    )
-                    yield subhdl
-            else:
-                self._log.debug(
-                    "Yielding %s of type %s (%s)", name, type(handle), handle._path
-                )
-                yield handle
+            yield handle
 
-    def _discover_all(self):
+    @lru_cache(maxsize=None)
+    def _discover_all(self) -> None:
         """When iterating or performing IPython tab completion, we run through ahead of
         time and discover all possible children, populating the :any:`_sub_handles`
         mapping. Hierarchy can't change after elaboration so we only have to
         do this once.
         """
-        if self._discovered:
-            return
-        self._log.debug("Discovering all on %s", self._name)
         for thing in self._handle.iterate(simulator.OBJECTS):
             name = thing.get_name_string()
             path = self._child_path(name)
@@ -237,8 +213,6 @@ def _discover_all(self):
 
             self._sub_handles[key] = hdl
 
-        self._discovered = True
-
     def _child_path(self, name) -> str:
         """Return a string of the path of the child :any:`SimHandle` for a given *name*."""
         return self._path + "." + name
@@ -253,9 +227,13 @@ def __dir__(self):
         return super().__dir__() + [str(k) for k in self._sub_handles]
 
 
-class HierarchyObject(RegionObject):
+class HierarchyObject(RegionObject[str]):
     """Hierarchy objects are namespace/scope objects."""
 
+    def __init__(self, handle, path) -> None:
+        super().__init__(handle, path)
+        self._invalid_sub_handles: Set[str] = set()
+
     def __get_sub_handle_by_name(self, name):
         try:
             return self._sub_handles[name]
@@ -288,7 +266,7 @@ def __setattr__(self, name, value):
 
         # private attributes pass through directly
         if name.startswith("_"):
-            return SimHandleBase.__setattr__(self, name, value)
+            return object.__setattr__(self, name, value)
 
         raise AttributeError(f"{self._name} contains no object named {name}")
 
@@ -297,7 +275,7 @@ def __getattr__(self, name):
         and cache the result to build a tree of objects.
         """
         if name.startswith("_"):
-            return SimHandleBase.__getattr__(self, name)
+            return object.__getattribute__(self, name)
 
         handle = self.__get_sub_handle_by_name(name)
         if handle is not None:
@@ -324,9 +302,12 @@ def _id(self, name, extended: bool = True):
         raise AttributeError(f"{self._name} contains no object named {name}")
 
 
-class HierarchyArrayObject(RegionObject):
+class HierarchyArrayObject(RegionObject[int]):
     """Hierarchy Arrays are containers of Hierarchy Objects."""
 
+    def __init__(self, handle, path) -> None:
+        super().__init__(handle, path)
+
     def _sub_handle_key(self, name):
         """Translate the handle name to a key to use in :any:`_sub_handles` dictionary."""
         # This is slightly hacky, but we need to extract the index from the name
@@ -348,14 +329,10 @@ def _sub_handle_key(self, name):
         else:
             raise ValueError(f"Unable to match an index pattern: {name}")
 
-    def __len__(self):
-        """Return the "length" of the generate block."""
-        if self._len is None:
-            if not self._discovered:
-                self._discover_all()
-
-            self._len = len(self._sub_handles)
-        return self._len
+    @lru_cache(maxsize=None)
+    def __len__(self) -> int:
+        self._discover_all()
+        return len(self._sub_handles)
 
     def __getitem__(self, index):
         if isinstance(index, slice):
@@ -381,9 +358,6 @@ def __setitem__(self, index, value):
 class NonHierarchyObject(SimHandleBase):
     """Common base class for all non-hierarchy objects."""
 
-    def __iter__(self):
-        return iter(())
-
     @property
     def value(self):
         """The value of this simulation object.
@@ -400,16 +374,25 @@ def value(self):
 
     @value.setter
     def value(self, value):
+        if self.is_const:
+            raise TypeError(f"{self._path} is constant")
         self._set_value(value, cocotb.scheduler._schedule_write)
 
     def setimmediatevalue(self, value):
         """Assign a value to this simulation object immediately."""
+        if self.is_const:
+            raise TypeError(f"{self._path} is constant")
 
         def _call_now(handle, f, *args):
             f(*args)
 
         self._set_value(value, _call_now)
 
+    @cached_property
+    def is_const(self) -> bool:
+        """``True`` if the simulator object is immutable, e.g. a Verilog parameter or VHDL constant or generic."""
+        return self._handle.get_const()
+
     def _set_value(self, value, call_sim):
         """This should be overriden in subclasses.
 
@@ -423,98 +406,16 @@ def _set_value(self, value, call_sim):
             f"Not permissible to set values on object {self._name} of type {type(self)}"
         )
 
-    def __eq__(self, other):
-        """Equality comparator for non-hierarchy objects
-
-        If ``other`` is not a :class:`SimHandleBase` instance the comparison
-        uses the comparison method of the ``other`` object against our
-        ``.value``.
-        """
-        if isinstance(other, SimHandleBase):
-            return SimHandleBase.__eq__(self, other)
-        return self.value == other
-
-    def __ne__(self, other):
-        if isinstance(other, SimHandleBase):
-            return SimHandleBase.__ne__(self, other)
-        return self.value != other
-
-    # Re-define hash because we defined __eq__
-    def __hash__(self):
-        return SimHandleBase.__hash__(self)
-
-
-class ConstantObject(NonHierarchyObject):
-    """An object which has a value that can be read, but not set.
-
-    The value is cached in the class since it is fixed at elaboration
-    time and won't change within a simulation.
-    """
-
-    def __init__(self, handle, path, handle_type):
-        """
-        Args:
-            handle (int): The GPI handle to the simulator object.
-            path (str): Path to this handle, ``None`` if root.
-            handle_type: The type of the handle
-                (``simulator.INTEGER``, ``simulator.ENUM``,
-                ``simulator.REAL``, ``simulator.STRING``).
-        """
-        NonHierarchyObject.__init__(self, handle, path)
-        if handle_type in [simulator.INTEGER, simulator.ENUM]:
-            self._value = self._handle.get_signal_val_long()
-        elif handle_type == simulator.REAL:
-            self._value = self._handle.get_signal_val_real()
-        elif handle_type == simulator.STRING:
-            self._value = self._handle.get_signal_val_str()
-        else:
-            val = self._handle.get_signal_val_binstr()
-            self._value = BinaryValue(n_bits=len(val))
-            try:
-                self._value.binstr = val
-            except Exception:
-                self._value = val
-
-    def __int__(self):
-        return int(self.value)
-
-    def __float__(self):
-        return float(self.value)
-
-    @NonHierarchyObject.value.getter
-    def value(self):
-        """The value of this simulation object."""
-        return self._value
-
-
-class NonHierarchyIndexableObject(NonHierarchyObject):
-    """A non-hierarchy indexable object.
-
-    Getting and setting the current value of an array is done
-    by iterating through sub-handles in left-to-right order.
-
-    Given an HDL array ``arr``:
-
-    +--------------+---------------------+--------------------------------------------------------------+
-    | Verilog      | VHDL                | ``arr.value`` is equivalent to                               |
-    +==============+=====================+==============================================================+
-    | ``arr[4:7]`` | ``arr(4 to 7)``     | ``[arr[4].value, arr[5].value, arr[6].value, arr[7].value]`` |
-    +--------------+---------------------+--------------------------------------------------------------+
-    | ``arr[7:4]`` | ``arr(7 downto 4)`` | ``[arr[7].value, arr[6].value, arr[5].value, arr[4].value]`` |
-    +--------------+---------------------+--------------------------------------------------------------+
-
-    When setting the signal as in ``arr.value = ...``, the same index equivalence as noted in the table holds.
-
-    .. warning::
-        Assigning a value to a sub-handle:
-
-        - **Wrong**: ``dut.some_array.value[0] = 1`` (gets value as a list then updates index 0)
-        - **Correct**: ``dut.some_array[0].value = 1``
-    """
 
+class NonHierarchyIndexableObjectBase(NonHierarchyObject):
+    @abstractmethod
     def __init__(self, handle, path):
-        NonHierarchyObject.__init__(self, handle, path)
-        self._range = self._handle.get_range()
+        super().__init__(handle, path)
+        self._sub_handles: Dict[int, SimHandleBase] = {}
+
+    @cached_property
+    def _range(self) -> Tuple[int, int]:
+        return self._handle.get_range()
 
     def __getitem__(self, index):
         if isinstance(index, slice):
@@ -539,7 +440,6 @@ def __iter__(self):
         if self._range is None:
             return
 
-        self._log.debug("Iterating with range [%d:%d]", self._range[0], self._range[1])
         for i in self._range_iter(self._range[0], self._range[1]):
             try:
                 result = self[i]
@@ -557,6 +457,39 @@ def _range_iter(self, left, right):
                 yield left
                 left = left + 1
 
+    @lru_cache(maxsize=None)
+    def __len__(self) -> int:
+        return self._handle.get_num_elems()
+
+
+class NonHierarchyIndexableObject(NonHierarchyIndexableObjectBase):
+    """A non-hierarchy indexable object.
+
+    Getting and setting the current value of an array is done
+    by iterating through sub-handles in left-to-right order.
+
+    Given an HDL array ``arr``:
+
+    +--------------+---------------------+--------------------------------------------------------------+
+    | Verilog      | VHDL                | ``arr.value`` is equivalent to                               |
+    +==============+=====================+==============================================================+
+    | ``arr[4:7]`` | ``arr(4 to 7)``     | ``[arr[4].value, arr[5].value, arr[6].value, arr[7].value]`` |
+    +--------------+---------------------+--------------------------------------------------------------+
+    | ``arr[7:4]`` | ``arr(7 downto 4)`` | ``[arr[7].value, arr[6].value, arr[5].value, arr[4].value]`` |
+    +--------------+---------------------+--------------------------------------------------------------+
+
+    When setting the signal as in ``arr.value = ...``, the same index equivalence as noted in the table holds.
+
+    .. warning::
+        Assigning a value to a sub-handle:
+
+        - **Wrong**: ``dut.some_array.value[0] = 1`` (gets value as a list then updates index 0)
+        - **Correct**: ``dut.some_array[0].value = 1``
+    """
+
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     @NonHierarchyObject.value.getter
     def value(self) -> list:
         # Don't use self.__iter__, because it has an unwanted `except IndexError`
@@ -583,28 +516,6 @@ def _set_value(self, value, call_sim):
             self[self_idx]._set_value(value[val_idx], call_sim)
 
 
-class NonConstantObject(NonHierarchyIndexableObject):
-    """A non-constant object"""
-
-    # FIXME: what is the difference to ModifiableObject? Explain in docstring.
-
-    def drivers(self):
-        """An iterator for gathering all drivers for a signal.
-
-        This is currently only available for VPI.
-        Also, only a few simulators implement this.
-        """
-        return self._handle.iterate(simulator.DRIVERS)
-
-    def loads(self):
-        """An iterator for gathering all loads on a signal.
-
-        This is currently only available for VPI.
-        Also, only a few simulators implement this.
-        """
-        return self._handle.iterate(simulator.LOADS)
-
-
 class _SetAction:
     """Base class representing the type of action used while write-accessing a handle."""
 
@@ -647,9 +558,37 @@ def _as_gpi_args_for(self, hdl):
         return 0, 2  # GPI_RELEASE
 
 
-class ModifiableObject(NonConstantObject):
+class ModifiableObject(NonHierarchyObject):
     """Base class for simulator objects whose values can be modified."""
 
+    def drivers(self):
+        """An iterator for gathering all drivers for a signal.
+
+        This is currently only available for VPI.
+        Also, only a few simulators implement this.
+        """
+        return self._handle.iterate(simulator.DRIVERS)
+
+    def loads(self):
+        """An iterator for gathering all loads on a signal.
+
+        This is currently only available for VPI.
+        Also, only a few simulators implement this.
+        """
+        return self._handle.iterate(simulator.LOADS)
+
+    def _check_for_set_action(self, value):
+        if not isinstance(value, _SetAction):
+            return value, 0  # GPI_DEPOSIT
+        return value._as_gpi_args_for(self)
+
+
+class LogicObject(ModifiableObject, NonHierarchyIndexableObjectBase):
+    """Specific object handle for Verilog nets and regs and VHDL std_logic and std_logic_vectors"""
+
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     def _set_value(self, value, call_sim):
         """Set the value of the underlying simulation object to *value*.
 
@@ -736,12 +675,7 @@ def _set_value(self, value, call_sim):
 
         call_sim(self, self._handle.set_signal_val_binstr, set_action, value.binstr)
 
-    def _check_for_set_action(self, value):
-        if not isinstance(value, _SetAction):
-            return value, 0  # GPI_DEPOSIT
-        return value._as_gpi_args_for(self)
-
-    @NonConstantObject.value.getter
+    @ModifiableObject.value.getter
     def value(self) -> BinaryValue:
         binstr = self._handle.get_signal_val_binstr()
         # Skip BinaryValue.assign() as we know we are using a binstr
@@ -750,13 +684,13 @@ def value(self) -> BinaryValue:
         result._set_trusted_binstr(binstr)
         return result
 
-    def __int__(self):
-        return int(self.value)
-
 
 class RealObject(ModifiableObject):
     """Specific object handle for Real signals and variables."""
 
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     def _set_value(self, value, call_sim):
         """Set the value of the underlying simulation object to value.
 
@@ -785,13 +719,13 @@ def _set_value(self, value, call_sim):
     def value(self) -> float:
         return self._handle.get_signal_val_real()
 
-    def __float__(self):
-        return float(self.value)
-
 
 class EnumObject(ModifiableObject):
     """Specific object handle for enumeration signals and variables."""
 
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     def _set_value(self, value, call_sim):
         """Set the value of the underlying simulation object to *value*.
 
@@ -832,6 +766,9 @@ def value(self) -> int:
 class IntegerObject(ModifiableObject):
     """Specific object handle for integer and enumeration signals and variables."""
 
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     def _set_value(self, value, call_sim):
         """Set the value of the underlying simulation object to *value*.
 
@@ -874,9 +811,12 @@ def value(self) -> int:
         return self._handle.get_signal_val_long()
 
 
-class StringObject(ModifiableObject):
+class StringObject(ModifiableObject, NonHierarchyIndexableObjectBase):
     """Specific object handle for String variables."""
 
+    def __init__(self, handle, path):
+        super().__init__(handle, path)
+
     def _set_value(self, value, call_sim):
         """Set the value of the underlying simulation object to *value*.
 
@@ -913,6 +853,19 @@ def value(self) -> bytes:
 
 _handle2obj = {}
 
+_type2cls = {
+    simulator.MODULE: HierarchyObject,
+    simulator.STRUCTURE: HierarchyObject,
+    simulator.REG: LogicObject,
+    simulator.NET: LogicObject,
+    simulator.NETARRAY: NonHierarchyIndexableObject,
+    simulator.REAL: RealObject,
+    simulator.INTEGER: IntegerObject,
+    simulator.ENUM: EnumObject,
+    simulator.STRING: StringObject,
+    simulator.GENARRAY: HierarchyArrayObject,
+}
+
 
 def SimHandle(handle, path=None):
     """Factory function to create the correct type of `SimHandle` object.
@@ -927,18 +880,6 @@ def SimHandle(handle, path=None):
     Raises:
         NotImplementedError: If no matching object for GPI type could be found.
     """
-    _type2cls = {
-        simulator.MODULE: HierarchyObject,
-        simulator.STRUCTURE: HierarchyObject,
-        simulator.REG: ModifiableObject,
-        simulator.NET: ModifiableObject,
-        simulator.NETARRAY: NonHierarchyIndexableObject,
-        simulator.REAL: RealObject,
-        simulator.INTEGER: IntegerObject,
-        simulator.ENUM: EnumObject,
-        simulator.STRING: StringObject,
-        simulator.GENARRAY: HierarchyArrayObject,
-    }
 
     # Enforce singletons since it's possible to retrieve handles avoiding
     # the hierarchy by getting driver/load information
@@ -948,18 +889,6 @@ def SimHandle(handle, path=None):
         pass
 
     t = handle.get_type()
-
-    # Special case for constants
-    if handle.get_const() and t not in [
-        simulator.MODULE,
-        simulator.STRUCTURE,
-        simulator.NETARRAY,
-        simulator.GENARRAY,
-    ]:
-        obj = ConstantObject(handle, path, t)
-        _handle2obj[handle] = obj
-        return obj
-
     if t not in _type2cls:
         raise NotImplementedError(
             "Couldn't find a matching object for GPI type %s(%d) (path=%s)"


Test patch:
diff --git a/examples/matrix_multiplier/tests/Makefile b/examples/matrix_multiplier/tests/Makefile
--- a/examples/matrix_multiplier/tests/Makefile
+++ b/examples/matrix_multiplier/tests/Makefile
@@ -63,8 +63,18 @@ export RANDOM_SEED := 123456789
 TOPLEVEL    := matrix_multiplier
 MODULE      := test_matrix_multiplier
 
+ifeq ($(SIM),ghdl)
+
+all:
+	@echo "Skipping since GHDL doesn't support constants effectively"
+
+clean::
+
+else
+
 include $(shell cocotb-config --makefiles)/Makefile.sim
 
+endif
 
 # Profiling
 
diff --git a/examples/matrix_multiplier/tests/test_matrix_multiplier.py b/examples/matrix_multiplier/tests/test_matrix_multiplier.py
--- a/examples/matrix_multiplier/tests/test_matrix_multiplier.py
+++ b/examples/matrix_multiplier/tests/test_matrix_multiplier.py
@@ -9,6 +9,7 @@
 from typing import Any, Dict, List
 
 import cocotb
+import pytest
 from cocotb.binary import BinaryValue
 from cocotb.clock import Clock
 from cocotb.handle import SimHandleBase
@@ -18,10 +19,10 @@
 
 NUM_SAMPLES = int(os.environ.get("NUM_SAMPLES", 3000))
 if cocotb.simulator.is_running():
-    DATA_WIDTH = int(cocotb.top.DATA_WIDTH)
-    A_ROWS = int(cocotb.top.A_ROWS)
-    B_COLUMNS = int(cocotb.top.B_COLUMNS)
-    A_COLUMNS_B_ROWS = int(cocotb.top.A_COLUMNS_B_ROWS)
+    DATA_WIDTH = cocotb.top.DATA_WIDTH.value
+    A_ROWS = cocotb.top.A_ROWS.value
+    B_COLUMNS = cocotb.top.B_COLUMNS.value
+    A_COLUMNS_B_ROWS = cocotb.top.A_COLUMNS_B_ROWS.value
 
 
 class DataValidMonitor:
@@ -145,11 +146,7 @@ async def _check(self) -> None:
             assert actual["C"] == expected
 
 
-@cocotb.test(
-    expect_error=IndexError
-    if cocotb.simulator.is_running() and cocotb.SIM_NAME.lower().startswith("ghdl")
-    else ()
-)
+@cocotb.test()
 async def multiply_test(dut):
     """Test multiplication of many matrices."""
 
@@ -214,6 +211,10 @@ def gen_b(num_samples=NUM_SAMPLES, func=getrandbits):
         yield create_b(func)
 
 
+@pytest.mark.skipif(
+    os.getenv("SIM", "icarus") == "ghdl",
+    reason="Skipping since GHDL doesn't support constants effectively",
+)
 def test_matrix_multiplier_runner():
     """Simulate the matrix_multiplier example using the Python runner.
 
diff --git a/examples/mixed_language/tests/test_mixed_language.py b/examples/mixed_language/tests/test_mixed_language.py
--- a/examples/mixed_language/tests/test_mixed_language.py
+++ b/examples/mixed_language/tests/test_mixed_language.py
@@ -41,7 +41,7 @@ async def mixed_language_accessing_test(dut):
     vhdl.reset_n.value = 1
     await Timer(100, units="ns")
 
-    assert int(verilog.reset_n) == int(vhdl.reset_n), "reset_n signals were different"
+    assert verilog.reset_n.value == vhdl.reset_n.value, "reset_n signals were different"
 
     # Try accessing an object other than a port...
     verilog.flush_pipe.value
@@ -95,7 +95,7 @@ async def mixed_language_functional_test(dut):
     for _ in range(1, 5):
         for i in range(1, 11):
             await RisingEdge(dut.clk)
-            previous_indata = dut.stream_in_data.value
+            previous_indata = dut.stream_in_data.value.integer
 
             # write stream in data
             dut.stream_in_data.value = i + 0x81FFFFFF2B00  # generate a magic number
@@ -108,8 +108,8 @@ async def mixed_language_functional_test(dut):
             await RisingEdge(dut.clk)
 
             # compare in and out data
-            assert int(previous_indata) == int(
-                dut.stream_out_data.value
+            assert (
+                previous_indata == dut.stream_out_data.value.integer
             ), f"stream in data and stream out data were different in round {i}"
 
 
diff --git a/tests/test_cases/issue_142/issue_142.py b/tests/test_cases/issue_142/issue_142.py
--- a/tests/test_cases/issue_142/issue_142.py
+++ b/tests/test_cases/issue_142/issue_142.py
@@ -17,13 +17,6 @@ async def issue_142_overflow_error(dut):
     through the GPI interface natively into BinaryValues"""
     cocotb.start_soon(Clock(dut.clk, 10, "ns").start())
 
-    def _compare(value):
-        assert int(dut.stream_in_data_wide.value) == int(
-            value
-        ), "Expecting 0x{:x} but got 0x{:x} on {}".format(
-            int(value), int(dut.stream_in_data_wide.value), str(dut.stream_in_data_wide)
-        )
-
     # Wider values are transparently converted to BinaryValues
     for value in [
         0,
@@ -33,7 +26,7 @@ def _compare(value):
     ]:
         dut.stream_in_data_wide.value = value
         await RisingEdge(dut.clk)
-        _compare(value)
+        assert dut.stream_in_data_wide.value.integer == value
         dut.stream_in_data_wide.value = value
         await RisingEdge(dut.clk)
-        _compare(value)
+        assert dut.stream_in_data_wide.value.integer == value
diff --git a/tests/test_cases/test_array/test_array.py b/tests/test_cases/test_array/test_array.py
--- a/tests/test_cases/test_array/test_array.py
+++ b/tests/test_cases/test_array/test_array.py
@@ -8,10 +8,9 @@
 import cocotb
 from cocotb.clock import Clock
 from cocotb.handle import (
-    ConstantObject,
     HierarchyArrayObject,
     HierarchyObject,
-    ModifiableObject,
+    LogicObject,
     NonHierarchyIndexableObject,
 )
 from cocotb.triggers import Timer
@@ -21,19 +20,21 @@
 
 def _check_type(tlog, hdl, expected):
     assert isinstance(hdl, expected), f">{hdl!r} ({hdl._type})< should be >{expected}<"
-    tlog.info("   Found %r (%s) with length=%d", hdl, hdl._type, len(hdl))
+    tlog.info("   Found %r (%s)", hdl, hdl._type)
 
 
 def _check_int(tlog, hdl, expected):
-    assert int(hdl) == expected, f"{hdl!r}: Expected >{expected}< but got >{int(hdl)}<"
-    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value={int(hdl)}")
+    assert hdl.value == expected, "{!r}: Expected >{}< but got >{}<".format(
+        hdl, expected, hdl.value
+    )
+    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value={hdl.value}")
 
 
 def _check_logic(tlog, hdl, expected):
-    assert int(hdl) == expected, "{!r}: Expected >0x{:X}< but got >0x{:X}<".format(
-        hdl, expected, int(hdl)
+    assert hdl.value == expected, "{!r}: Expected >{}< but got >{}<".format(
+        hdl, expected, hdl.value
     )
-    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value=0x{int(hdl):X}")
+    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value={hdl.value}")
 
 
 def _check_str(tlog, hdl, expected):
@@ -44,10 +45,10 @@ def _check_str(tlog, hdl, expected):
 
 
 def _check_real(tlog, hdl, expected):
-    assert float(hdl) == expected, "{!r}: Expected >{}< but got >{}<".format(
-        hdl, expected, float(hdl)
+    assert hdl.value == expected, "{!r}: Expected >{}< but got >{}<".format(
+        hdl, expected, hdl.value
     )
-    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value={float(hdl)}")
+    tlog.info(f"   Found {hdl!r} ({hdl._type}) with value={hdl.value}")
 
 
 def _check_value(tlog, hdl, expected):
@@ -281,14 +282,31 @@ async def test_discover_all(dut):
                     30 (port_rec_out)                                          (VPI - Aldec sees as a Module and not structure (i.e. cnt = 1))
                     61 (port_cmplx_out)                                        (VPI - Aldec sees as a Module and not structure (i.e. cnt = 1))
         constants:   1 (const_logic)
-                     1 (const_logic_vec)
+                     9 (const_logic_vec)
                      1 (const_bool)                                            (VHDL Only)
                      1 (const_int)                                             (VHDL Only)
                      1 (const_real)                                            (VHDL Only)
                      1 (const_char)                                            (VHDL Only)
-                     1 (const_str)                                             (VHDL Only)
-                     6 (const_rec.a, const_rec.b[0:2])                         (VHDL Only)
-                    13 (const_cmplx[1:2].a, const_cmplx[1:2].b[0:2])           (VHDL Only)
+                     9 (const_str)                                             (VHDL Only)
+                     1 (const_rec)                                             (VHDL Only)
+                     1 (const_rec.a)                                           (VHDL Only)
+                     1 (const_rec.b)                                           (VHDL Only)
+                     9 (const_rec.b[0])                                        (VHDL Only)
+                     9 (const_rec.b[1])                                        (VHDL Only)
+                     9 (const_rec.b[2])                                        (VHDL Only)
+                     1 (const_cmplx)                                           (VHDL Only)
+                     1 (const_cmplx[1])                                        (VHDL Only)
+                     1 (const_cmplx[1].a)                                      (VHDL Only)
+                     1 (const_cmplx[1].b)                                      (VHDL Only)
+                     9 (const_cmplx[1].b[0])                                   (VHDL Only)
+                     9 (const_cmplx[1].b[1])                                   (VHDL Only)
+                     9 (const_cmplx[1].b[2])                                   (VHDL Only)
+                     1 (const_cmplx[2])                                        (VHDL Only)
+                     1 (const_cmplx[2].a)                                      (VHDL Only)
+                     1 (const_cmplx[2].b)                                      (VHDL Only)
+                     9 (const_cmplx[2].b[0])                                   (VHDL Only)
+                     9 (const_cmplx[2].b[1])                                   (VHDL Only)
+                     9 (const_cmplx[2].b[2])                                   (VHDL Only)
           signals:   9 (sig_desc)
                      9 (sig_asc)
                      1 (\ext_id\)                                              (VHDL Only)
@@ -323,7 +341,7 @@ async def test_discover_all(dut):
                      8 (desc_gen: process "always")                            (VPI - Aldec only)
           process:   1 ("always")                                              (VPI - Aldec only)
 
-            TOTAL:  856 (VHDL - Default)
+            TOTAL: 1032 (VHDL - Default)
                     818 (VHDL - Aldec)
                    1078 (Verilog - Default)
                947/1038 (Verilog - Aldec)
@@ -361,9 +379,10 @@ async def test_discover_all(dut):
         and cocotb.SIM_NAME.lower().startswith("modelsim")
         and os.environ["VHDL_GPI_INTERFACE"] == "vhpi"
     ):
-        pass_total = 920
+        # VHPI finds the array_module.asc_gen and array_module.desc_gen more than once =/
+        pass_total = 1096
     elif cocotb.LANGUAGE in ["vhdl"]:
-        pass_total = 856
+        pass_total = 1032
     elif cocotb.LANGUAGE in ["verilog"] and cocotb.SIM_NAME.lower().startswith(
         "riviera"
     ):
@@ -376,17 +395,24 @@ async def test_discover_all(dut):
     else:
         pass_total = 1078
 
-    def _discover(obj, indent):
+    def _discover(obj):
+        if not isinstance(
+            obj,
+            (
+                cocotb.handle.RegionObject,
+                cocotb.handle.NonHierarchyIndexableObjectBase,
+            ),
+        ):
+            return 0
         count = 0
-        new_indent = indent + "---"
         for thing in obj:
             count += 1
-            tlog.info("%sFound %r (%s)", indent, thing, thing._type)
-            count += _discover(thing, new_indent)
+            tlog.info("Found %s (%s)", thing._path, type(thing))
+            count += _discover(thing)
         return count
 
     tlog.info("Iterating over %r (%s)", dut, dut._type)
-    total = _discover(dut, "")
+    total = _discover(dut)
     tlog.info("Found a total of %d things", total)
     assert total == pass_total
 
@@ -405,26 +431,26 @@ async def test_direct_constant_indexing(dut):
 
     tlog.info("Checking Types of complex array structures in constants/parameters.")
     _check_type(tlog, dut.param_rec, HierarchyObject)
-    _check_type(tlog, dut.param_rec.a, ConstantObject)
+    _check_type(tlog, dut.param_rec.a, LogicObject)
     _check_type(tlog, dut.param_rec.b, NonHierarchyIndexableObject)
-    _check_type(tlog, dut.param_rec.b[1], ConstantObject)
+    _check_type(tlog, dut.param_rec.b[1], LogicObject)
 
     _check_type(tlog, dut.param_cmplx, NonHierarchyIndexableObject)
     _check_type(tlog, dut.param_cmplx[0], HierarchyObject)
-    _check_type(tlog, dut.param_cmplx[0].a, ConstantObject)
+    _check_type(tlog, dut.param_cmplx[0].a, LogicObject)
     _check_type(tlog, dut.param_cmplx[0].b, NonHierarchyIndexableObject)
-    _check_type(tlog, dut.param_cmplx[0].b[1], ConstantObject)
+    _check_type(tlog, dut.param_cmplx[0].b[1], LogicObject)
 
     _check_type(tlog, dut.const_rec, HierarchyObject)
-    _check_type(tlog, dut.const_rec.a, ConstantObject)
+    _check_type(tlog, dut.const_rec.a, LogicObject)
     _check_type(tlog, dut.const_rec.b, NonHierarchyIndexableObject)
-    _check_type(tlog, dut.const_rec.b[1], ConstantObject)
+    _check_type(tlog, dut.const_rec.b[1], LogicObject)
 
     _check_type(tlog, dut.const_cmplx, NonHierarchyIndexableObject)
     _check_type(tlog, dut.const_cmplx[1], HierarchyObject)
-    _check_type(tlog, dut.const_cmplx[1].a, ConstantObject)
+    _check_type(tlog, dut.const_cmplx[1].a, LogicObject)
     _check_type(tlog, dut.const_cmplx[1].b, NonHierarchyIndexableObject)
-    _check_type(tlog, dut.const_cmplx[1].b[1], ConstantObject)
+    _check_type(tlog, dut.const_cmplx[1].b[1], LogicObject)
 
 
 # GHDL unable to index packed arrays (gh-2587)
@@ -449,30 +475,30 @@ async def test_direct_signal_indexing(dut):
     await Timer(20, "ns")
 
     tlog.info("Checking bit mapping from input to generate loops.")
-    assert int(dut.desc_gen[2].sig) == 1
-    tlog.info("   %r = %d", dut.desc_gen[2].sig, int(dut.desc_gen[2].sig))
+    assert dut.desc_gen[2].sig.value == 1
+    tlog.info("   %r = %d", dut.desc_gen[2].sig, dut.desc_gen[2].sig.value)
 
-    assert int(dut.asc_gen[18].sig) == 1
-    tlog.info("   %r = %d", dut.asc_gen[18].sig, int(dut.asc_gen[18].sig))
+    assert dut.asc_gen[18].sig.value == 1
+    tlog.info("   %r = %d", dut.asc_gen[18].sig, dut.asc_gen[18].sig.value)
 
     tlog.info("Checking indexing of data with offset index.")
-    assert int(dut.port_ofst_out) == 64
+    assert dut.port_ofst_out.value == 64
     tlog.info(
         "   %r = %d (%s)",
         dut.port_ofst_out,
-        int(dut.port_ofst_out),
+        dut.port_ofst_out.value,
         dut.port_ofst_out.value.binstr,
     )
 
     tlog.info("Checking Types of complex array structures in signals.")
-    _check_type(tlog, dut.sig_desc[20], ModifiableObject)
-    _check_type(tlog, dut.sig_asc[17], ModifiableObject)
-    _check_type(tlog, dut.sig_t1, ModifiableObject)
+    _check_type(tlog, dut.sig_desc[20], LogicObject)
+    _check_type(tlog, dut.sig_asc[17], LogicObject)
+    _check_type(tlog, dut.sig_t1, LogicObject)
     _check_type(tlog, dut.sig_t2, NonHierarchyIndexableObject)
-    _check_type(tlog, dut.sig_t2[5], ModifiableObject)
-    _check_type(tlog, dut.sig_t2[5][3], ModifiableObject)
-    _check_type(tlog, dut.sig_t3a[2][3], ModifiableObject)
-    _check_type(tlog, dut.sig_t3b[3], ModifiableObject)
+    _check_type(tlog, dut.sig_t2[5], LogicObject)
+    _check_type(tlog, dut.sig_t2[5][3], LogicObject)
+    _check_type(tlog, dut.sig_t3a[2][3], LogicObject)
+    _check_type(tlog, dut.sig_t3b[3], LogicObject)
     _check_type(tlog, dut.sig_t3a, NonHierarchyIndexableObject)
     _check_type(tlog, dut.sig_t4, NonHierarchyIndexableObject)
     _check_type(tlog, dut.sig_t4[3], NonHierarchyIndexableObject)
@@ -482,12 +508,12 @@ async def test_direct_signal_indexing(dut):
         and cocotb.SIM_NAME.lower().startswith("riviera")
         and cocotb.SIM_VERSION.startswith(("2016.06", "2016.10", "2017.02"))
     ):
-        _check_type(tlog, dut.sig_t4[3][4], ModifiableObject)
-        _check_type(tlog, dut.sig_t4[3][4][1], ModifiableObject)
+        _check_type(tlog, dut.sig_t4[3][4], LogicObject)
+        _check_type(tlog, dut.sig_t4[3][4][1], LogicObject)
     _check_type(tlog, dut.sig_t5, NonHierarchyIndexableObject)
     _check_type(tlog, dut.sig_t5[1], NonHierarchyIndexableObject)
-    _check_type(tlog, dut.sig_t5[1][0], ModifiableObject)
-    _check_type(tlog, dut.sig_t5[1][0][6], ModifiableObject)
+    _check_type(tlog, dut.sig_t5[1][0], LogicObject)
+    _check_type(tlog, dut.sig_t5[1][0][6], LogicObject)
     _check_type(tlog, dut.sig_t6, NonHierarchyIndexableObject)
     _check_type(tlog, dut.sig_t6[1], NonHierarchyIndexableObject)
     # the following version cannot index into those arrays and will error out
@@ -496,15 +522,17 @@ async def test_direct_signal_indexing(dut):
         and cocotb.SIM_NAME.lower().startswith("riviera")
         and cocotb.SIM_VERSION.startswith(("2016.06", "2016.10", "2017.02"))
     ):
-        _check_type(tlog, dut.sig_t6[0][3], ModifiableObject)
-        _check_type(tlog, dut.sig_t6[0][3][7], ModifiableObject)
+        _check_type(tlog, dut.sig_t6[0][3], LogicObject)
+        _check_type(tlog, dut.sig_t6[0][3][7], LogicObject)
     _check_type(tlog, dut.sig_cmplx, NonHierarchyIndexableObject)
 
     if cocotb.LANGUAGE in ["verilog"]:
         _check_type(tlog, dut.sig_t7[1], NonHierarchyIndexableObject)
-        _check_type(tlog, dut.sig_t7[0][3], ModifiableObject)
-        _check_type(tlog, dut.sig_t8[1], NonHierarchyIndexableObject)
-        _check_type(tlog, dut.sig_t8[0][3], ModifiableObject)
+        _check_type(tlog, dut.sig_t7[0][3], LogicObject)
+        _check_type(
+            tlog, dut.sig_t8[1], LogicObject
+        )  # packed array of logic is mapped to GPI_NET
+        _check_type(tlog, dut.sig_t8[0][3], LogicObject)
 
     # Riviera has a bug and finds dut.sig_cmplx[1], but the type returned is a vpiBitVar
     # only true for version 2016.02
@@ -514,13 +542,13 @@ async def test_direct_signal_indexing(dut):
         and cocotb.SIM_VERSION.startswith("2016.02")
     ):
         _check_type(tlog, dut.sig_cmplx[1], HierarchyObject)
-        _check_type(tlog, dut.sig_cmplx[1].a, ModifiableObject)
+        _check_type(tlog, dut.sig_cmplx[1].a, LogicObject)
         _check_type(tlog, dut.sig_cmplx[1].b, NonHierarchyIndexableObject)
-        _check_type(tlog, dut.sig_cmplx[1].b[1], ModifiableObject)
-        _check_type(tlog, dut.sig_cmplx[1].b[1][2], ModifiableObject)
+        _check_type(tlog, dut.sig_cmplx[1].b[1], LogicObject)
+        _check_type(tlog, dut.sig_cmplx[1].b[1][2], LogicObject)
 
     _check_type(tlog, dut.sig_rec, HierarchyObject)
-    _check_type(tlog, dut.sig_rec.a, ModifiableObject)
+    _check_type(tlog, dut.sig_rec.a, LogicObject)
     _check_type(tlog, dut.sig_rec.b, NonHierarchyIndexableObject)
 
     # Riviera has a bug and finds dut.sig_rec.b[1], but the type returned is 0 which is unknown
@@ -530,8 +558,8 @@ async def test_direct_signal_indexing(dut):
         and cocotb.SIM_NAME.lower().startswith("riviera")
         and cocotb.SIM_VERSION.startswith("2016.02")
     ):
-        _check_type(tlog, dut.sig_rec.b[1], ModifiableObject)
-        _check_type(tlog, dut.sig_rec.b[1][2], ModifiableObject)
+        _check_type(tlog, dut.sig_rec.b[1], LogicObject)
+        _check_type(tlog, dut.sig_rec.b[1][2], LogicObject)
 
 
 @cocotb.test(skip=(cocotb.LANGUAGE in ["verilog"]))
@@ -540,5 +568,5 @@ async def test_extended_identifiers(dut):
 
     tlog = logging.getLogger("cocotb.test")
     tlog.info("Checking extended identifiers.")
-    _check_type(tlog, dut._id("\\ext_id\\", extended=False), ModifiableObject)
-    _check_type(tlog, dut._id("!"), ModifiableObject)
+    _check_type(tlog, dut._id("\\ext_id\\", extended=False), LogicObject)
+    _check_type(tlog, dut._id("!"), LogicObject)
diff --git a/tests/test_cases/test_cocotb/test_handle.py b/tests/test_cases/test_cocotb/test_handle.py
--- a/tests/test_cases/test_cocotb/test_handle.py
+++ b/tests/test_cases/test_cocotb/test_handle.py
@@ -348,7 +348,7 @@ async def test_real_assign_double(dut):
     dut.stream_in_real.value = val
     await timer_shortest
     await timer_shortest  # FIXME: Workaround for VHPI scheduling - needs investigation
-    got = float(dut.stream_out_real)
+    got = dut.stream_out_real.value
     log.info("Read back value %g" % got)
     assert got == val, "Values didn't match!"
 
@@ -374,9 +374,9 @@ async def test_real_assign_int(dut):
     dut.stream_in_real.value = val
     await timer_shortest
     await timer_shortest  # FIXME: Workaround for VHPI scheduling - needs investigation
-    got = dut.stream_out_real
+    got = dut.stream_out_real.value
     log.info("Read back value %d" % got)
-    assert got == float(val), "Values didn't match!"
+    assert got == val, "Values didn't match!"
 
 
 # identifiers starting with `_` are illegal in VHDL
diff --git a/tests/test_cases/test_compare/test_compare.py b/tests/test_cases/test_compare/test_compare.py
--- a/tests/test_cases/test_compare/test_compare.py
+++ b/tests/test_cases/test_compare/test_compare.py
@@ -77,8 +77,8 @@ async def test_compare_nonhierarchy(dut):
     # A handle and a value
     # Because one is a value, it is compared against the value of the handle
     await tb.clkedge
-    assert dut.clk == 1
-    assert dut.clk != 0
+    assert dut.clk.value == 1
+    assert dut.clk.value != 0
     await FallingEdge(tb.dut.clk)
-    assert dut.clk == 0
-    assert dut.clk != 1
+    assert dut.clk.value == 0
+    assert dut.clk.value != 1
diff --git a/tests/test_cases/test_discovery/test_discovery.py b/tests/test_cases/test_discovery/test_discovery.py
--- a/tests/test_cases/test_discovery/test_discovery.py
+++ b/tests/test_cases/test_discovery/test_discovery.py
@@ -25,11 +25,20 @@
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+import os
+
 import cocotb
 import pytest
 from cocotb._sim_versions import IcarusVersion
 from cocotb.binary import BinaryValue
-from cocotb.handle import ConstantObject, HierarchyObject, IntegerObject, StringObject
+from cocotb.handle import (
+    HierarchyObject,
+    IntegerObject,
+    NonHierarchyIndexableObject,
+    NonHierarchyIndexableObjectBase,
+    RegionObject,
+    StringObject,
+)
 from cocotb.triggers import Timer
 
 
@@ -52,6 +61,8 @@ async def recursive_discover(dut):
     """Discover absolutely everything in the DUT"""
 
     def _discover(obj):
+        if not isinstance(obj, (HierarchyObject, NonHierarchyIndexableObject)):
+            return
         for thing in obj:
             dut._log.debug("Found %s (%s)", thing._name, type(thing))
             _discover(thing)
@@ -187,11 +198,6 @@ async def access_integer(dut):
     """Integer should show as an IntegerObject"""
     assert isinstance(dut.stream_in_int, IntegerObject)
 
-    with pytest.raises(IndexError):
-        dut.stream_in_int[3]
-
-    assert len(dut.stream_in_int) == 1
-
 
 @cocotb.test(skip=cocotb.LANGUAGE in ["verilog"])
 async def access_ulogic(dut):
@@ -199,24 +205,32 @@ async def access_ulogic(dut):
     dut.stream_in_valid
 
 
-@cocotb.test(skip=cocotb.LANGUAGE in ["verilog"])
+# GHDL discovers generics as vpiParameter (gh-2722)
+@cocotb.test(
+    skip=cocotb.LANGUAGE in ["verilog"],
+    expect_error=NotImplementedError
+    if cocotb.SIM_NAME.lower().startswith("ghdl")
+    else (),
+)
 async def access_constant_integer(dut):
     """
     Access a constant integer
     """
-    assert isinstance(dut.isample_module1.EXAMPLE_WIDTH, ConstantObject)
-    assert dut.isample_module1.EXAMPLE_WIDTH == 7
+    assert isinstance(dut.isample_module1.EXAMPLE_WIDTH, IntegerObject)
+    assert dut.isample_module1.EXAMPLE_WIDTH.value == 7
 
 
-# GHDL inexplicably crashes, so we will skip this test for now
-# likely has to do with overall poor support of string over the VPI
+# GHDL discovers generics as vpiParameter (gh-2722)
 @cocotb.test(
-    skip=cocotb.LANGUAGE in ["verilog"] or cocotb.SIM_NAME.lower().startswith("ghdl")
+    skip=cocotb.LANGUAGE in ["verilog"],
+    expect_error=NotImplementedError
+    if cocotb.SIM_NAME.lower().startswith("ghdl")
+    else (),
 )
 async def access_constant_string_vhdl(dut):
     """Access to a string, both constant and signal."""
     constant_string = dut.isample_module1.EXAMPLE_STRING
-    assert isinstance(constant_string, ConstantObject)
+    assert isinstance(constant_string, StringObject)
     assert constant_string.value == b"TESTING"
 
 
@@ -228,7 +242,7 @@ async def access_constant_string_vhdl(dut):
 async def test_writing_string_undersized(dut):
     test_string = b"cocotb"
     dut.stream_in_string.setimmediatevalue(test_string)
-    assert dut.stream_out_string == b""
+    assert dut.stream_out_string.value == b""
     await Timer(1, "ns")
     assert dut.stream_out_string.value == test_string
 
@@ -294,11 +308,11 @@ async def access_const_string_verilog(dut):
 
     await Timer(10, "ns")
     assert isinstance(dut.STRING_CONST, StringObject)
-    assert dut.STRING_CONST == b"TESTING_CONST"
+    assert dut.STRING_CONST.value == b"TESTING_CONST"
 
     dut.STRING_CONST.value = b"MODIFIED"
     await Timer(10, "ns")
-    assert dut.STRING_CONST != b"TESTING_CONST"
+    assert dut.STRING_CONST.value != b"TESTING_CONST"
 
 
 @cocotb.test(
@@ -310,28 +324,35 @@ async def access_var_string_verilog(dut):
 
     await Timer(10, "ns")
     assert isinstance(dut.STRING_VAR, StringObject)
-    assert dut.STRING_VAR == b"TESTING_VAR"
+    assert dut.STRING_VAR.value == b"TESTING_VAR"
 
     dut.STRING_VAR.value = b"MODIFIED"
     await Timer(10, "ns")
-    assert dut.STRING_VAR == b"MODIFIED"
+    assert dut.STRING_VAR.value == b"MODIFIED"
 
 
-@cocotb.test(skip=cocotb.LANGUAGE in ["verilog"])
+# GHDL discovers generics as vpiParameter (gh-2722)
+@cocotb.test(
+    skip=cocotb.LANGUAGE in ["verilog"],
+    expect_error=NotImplementedError
+    if cocotb.SIM_NAME.lower().startswith("ghdl")
+    else (),
+)
 async def access_constant_boolean(dut):
     """Test access to a constant boolean"""
-    assert isinstance(dut.isample_module1.EXAMPLE_BOOL, ConstantObject)
+    assert isinstance(dut.isample_module1.EXAMPLE_BOOL, IntegerObject)
     assert dut.isample_module1.EXAMPLE_BOOL.value == True  # noqa
 
 
-@cocotb.test(skip=cocotb.LANGUAGE in ["verilog"])
+# GHDL discovers booleans as vpiNet (gh-2596)
+@cocotb.test(
+    skip=cocotb.LANGUAGE in ["verilog"],
+    expect_fail=cocotb.SIM_NAME.lower().startswith("ghdl"),
+)
+@cocotb.test()
 async def access_boolean(dut):
     """Test access to a boolean"""
-
-    with pytest.raises(IndexError):
-        dut.stream_in_bool[3]
-
-    assert len(dut.stream_in_bool) == 1
+    assert isinstance(dut.stream_out_bool, IntegerObject)
 
     curr_val = dut.stream_in_bool.value
     dut.stream_in_bool.setimmediatevalue(not curr_val)
@@ -380,6 +401,8 @@ async def custom_type(dut):
     count = 0
 
     def _discover(obj):
+        if not isinstance(obj, (RegionObject, NonHierarchyIndexableObjectBase)):
+            return 0
         iter_count = 0
         for elem in obj:
             iter_count += 1
@@ -450,7 +473,17 @@ async def access_block_vhdl(dut):
 async def discover_all_in_component_vhdl(dut):
     """Access a non local indexed name"""
 
+    questa_vhpi = (
+        cocotb.SIM_NAME.lower().startswith("modelsim")
+        and os.getenv("VHDL_GPI_INTERFACE", "fli") == "vhpi"
+    )
+
     def _discover(obj):
+        if questa_vhpi and isinstance(obj, StringObject):
+            # Iterating over the elements of a string with Questa's VHPI causes a stacktrace
+            return 0
+        if not isinstance(obj, (RegionObject, NonHierarchyIndexableObjectBase)):
+            return 0
         count = 0
         for thing in obj:
             count += 1
@@ -462,8 +495,9 @@ def _discover(obj):
 
     sim = cocotb.SIM_NAME.lower()
 
-    # ideally should be 25:
+    # ideally should be 32:
     #   1   EXAMPLE_STRING
+    #   7   EXAMPLE_STRING[*]
     #   1   EXAMPLE_BOOL
     #   1   EXAMPLE_WIDTH
     #   1   clk
@@ -474,17 +508,20 @@ def _discover(obj):
     #   1   stream_out_data_valid
     #   1   SAMPLE_BLOCK
     #   1   SAMPLE_BLOCK.clk_inv
-    if sim.startswith("modelsim"):
+    if sim.startswith("modelsim") and questa_vhpi:
+        # Iterating over the elements of a string with Questa's VHPI causes a stacktrace
         assert total_count == 25
+    elif sim.startswith("modelsim"):
+        assert total_count == 32
     elif sim.startswith("riviera"):
-        assert total_count == 25
+        assert total_count == 32
     elif sim.startswith("xcelium"):
-        assert total_count == 25
+        assert total_count == 32
     elif sim.startswith("ghdl"):
         # finds SAMPLE_BLOCK twice
-        # doesn't find EXAMPLE_STRING or elements
+        # doesn't find EXAMPLE_STRING or elements, EXAMPLE_BOOL, or EXAMPLE_WIDTH
         # doesn't find elements of stream_in_data or stream_out_data_registered
-        assert total_count == 9
+        assert total_count == 7
     elif sim.startswith("nvc"):
         # doesn't find elements of string
         # finds SAMPLE_BLOCK.clk_inv twice?
diff --git a/tests/test_cases/test_force_release/test_force_release.py b/tests/test_cases/test_force_release/test_force_release.py
--- a/tests/test_cases/test_force_release/test_force_release.py
+++ b/tests/test_cases/test_force_release/test_force_release.py
@@ -15,8 +15,8 @@ async def test_force_release(dut):
     dut.stream_in_data.value = 4
     dut.stream_out_data_comb.value = Force(5)
     await Timer(10, "ns")
-    got_in = int(dut.stream_in_data)
-    got_out = int(dut.stream_out_data_comb)
+    got_in = dut.stream_in_data.value.integer
+    got_out = dut.stream_out_data_comb.value.integer
     log.info("dut.stream_in_data = %d" % got_in)
     log.info("dut.stream_out_data_comb = %d" % got_out)
     assert got_in != got_out
@@ -24,8 +24,8 @@ async def test_force_release(dut):
     dut.stream_out_data_comb.value = Release()
     dut.stream_in_data.value = 3
     await Timer(10, "ns")
-    got_in = int(dut.stream_in_data)
-    got_out = int(dut.stream_out_data_comb)
+    got_in = dut.stream_in_data.value.integer
+    got_out = dut.stream_out_data_comb.value.integer
     log.info("dut.stream_in_data = %d" % got_in)
     log.info("dut.stream_out_data_comb = %d" % got_out)
     assert got_in == got_out
diff --git a/tests/test_cases/test_iteration_mixedlang/test_iteration.py b/tests/test_cases/test_iteration_mixedlang/test_iteration.py
--- a/tests/test_cases/test_iteration_mixedlang/test_iteration.py
+++ b/tests/test_cases/test_iteration_mixedlang/test_iteration.py
@@ -34,10 +34,15 @@ def recursive_dump(parent, log):
 
     Returns a count of the total number of objects found
     """
+    if not isinstance(
+        parent,
+        (cocotb.handle.RegionObject, cocotb.handle.NonHierarchyIndexableObjectBase),
+    ):
+        return 0
     count = 0
     for thing in parent:
         count += 1
-        log.info("Found %s.%s (%s)", parent._name, thing._name, type(thing))
+        log.info("Found %s (%s)", thing._path, type(thing))
         count += recursive_dump(thing, log)
     return count
 
@@ -78,7 +83,7 @@ async def recursive_discovery(dut):
         # vpiAlways = 31 and vpiStructVar = 2 do not show up in IUS/Xcelium
         pass_total = 975
     elif cocotb.SIM_NAME.lower().startswith("modelsim"):
-        pass_total = 991
+        pass_total = 1332
     else:
         pass_total = 1024
 
@@ -89,7 +94,7 @@ async def recursive_discovery(dut):
     tlog.info("Found a total of %d things", total)
 
     assert isinstance(
-        dut.i_verilog.uart1.baud_gen_1.baud_freq, cocotb.handle.ModifiableObject
+        dut.i_verilog.uart1.baud_gen_1.baud_freq, cocotb.handle.LogicObject
     ), (
         "Expected dut.i_verilog.uart1.baud_gen_1.baud_freq to be modifiable"
         " but it was %s" % type(dut.i_verilog.uart1.baud_gen_1.baud_freq).__name__
@@ -105,7 +110,7 @@ async def recursive_discovery_boundary(dut):
     if cocotb.SIM_NAME.lower().startswith(("ncsim", "xmsim")):
         pass_total = 462
     else:
-        pass_total = 478
+        pass_total = 819
 
     tlog = logging.getLogger("cocotb.test")
     total = recursive_dump(dut.i_vhdl, tlog)
diff --git a/tests/test_cases/test_iteration_verilog/test_iteration_es.py b/tests/test_cases/test_iteration_verilog/test_iteration_es.py
--- a/tests/test_cases/test_iteration_verilog/test_iteration_es.py
+++ b/tests/test_cases/test_iteration_verilog/test_iteration_es.py
@@ -45,10 +45,18 @@ async def recursive_discovery(dut):
     tlog = logging.getLogger("cocotb.test")
 
     def dump_all_the_things(parent):
+        if not isinstance(
+            parent,
+            (
+                cocotb.handle.RegionObject,
+                cocotb.handle.NonHierarchyIndexableObjectBase,
+            ),
+        ):
+            return 0
         count = 0
         for thing in parent:
             count += 1
-            tlog.info("Found %s.%s (%s)", parent._name, thing._name, type(thing))
+            tlog.info("Found %s (%s)", thing._path, type(thing))
             count += dump_all_the_things(thing)
         return count
 
diff --git a/tests/test_cases/test_iteration_vhdl/test_iteration.py b/tests/test_cases/test_iteration_vhdl/test_iteration.py
--- a/tests/test_cases/test_iteration_vhdl/test_iteration.py
+++ b/tests/test_cases/test_iteration_vhdl/test_iteration.py
@@ -39,7 +39,7 @@ def total_object_count():
     # Questa with VHPI
     # TODO: Why do we get massively different numbers for Questa/VHPI than for Questa/FLI or VPI?
     if SIM_NAME.startswith("modelsim") and os.environ["VHDL_GPI_INTERFACE"] == "vhpi":
-        return 66959
+        return 68127
 
     # Questa 2023.1 onwards (FLI) do not discover the following objects, which
     # are instantiated four times:
@@ -52,7 +52,7 @@ def total_object_count():
         and QuestaVersion(SIM_VERSION) >= QuestaVersion("2023.1")
         and os.environ["VHDL_GPI_INTERFACE"] == "fli"
     ):
-        return 34569 - 4 * 4
+        return 35153 - 4 * 4
 
     if SIM_NAME.startswith(
         (
@@ -62,7 +62,7 @@ def total_object_count():
             "riviera",
         )
     ):
-        return 34569
+        return 35153
 
     # Active-HDL
     if SIM_NAME.startswith("aldec"):
@@ -86,10 +86,18 @@ async def recursive_discovery(dut):
     await Timer(100)
 
     def dump_all_the_things(parent):
+        if not isinstance(
+            parent,
+            (
+                cocotb.handle.RegionObject,
+                cocotb.handle.NonHierarchyIndexableObjectBase,
+            ),
+        ):
+            return 0
         count = 0
         for thing in parent:
             count += 1
-            tlog.debug("Found %s.%s (%s)", parent._name, thing._name, type(thing))
+            tlog.info("Found %s (%s)", thing._path, type(thing))
             count += dump_all_the_things(thing)
         return count
 
diff --git a/tests/test_cases/test_verilog_access/test_verilog_access.py b/tests/test_cases/test_verilog_access/test_verilog_access.py
--- a/tests/test_cases/test_verilog_access/test_verilog_access.py
+++ b/tests/test_cases/test_verilog_access/test_verilog_access.py
@@ -26,14 +26,14 @@
 import logging
 
 import cocotb
-from cocotb.handle import HierarchyObject, ModifiableObject
+from cocotb.handle import HierarchyObject, LogicObject
 
 
 @cocotb.test()
 async def port_not_hierarchy(dut):
     """
     Test for issue raised by Luke - iteration causes a toplevel port type to
-    change from ModifiableObject to HierarchyObject
+    change from LogicObject to HierarchyObject
     """
     fails = 0
     tlog = logging.getLogger("cocotb.test")
@@ -49,10 +49,10 @@ def check_instance(obj, objtype):
         tlog.info(f"{obj._fullname} is {type(obj).__name__}")
         return 0
 
-    fails += check_instance(dut.clk, ModifiableObject)
+    fails += check_instance(dut.clk, LogicObject)
     fails += check_instance(dut.i_verilog, HierarchyObject)
-    fails += check_instance(dut.i_verilog.clock, ModifiableObject)
-    fails += check_instance(dut.i_verilog.tx_data, ModifiableObject)
+    fails += check_instance(dut.i_verilog.clock, LogicObject)
+    fails += check_instance(dut.i_verilog.tx_data, LogicObject)
 
     for _ in dut:
         pass
@@ -60,9 +60,9 @@ def check_instance(obj, objtype):
     for _ in dut.i_verilog:
         pass
 
-    fails += check_instance(dut.clk, ModifiableObject)
+    fails += check_instance(dut.clk, LogicObject)
     fails += check_instance(dut.i_verilog, HierarchyObject)
-    fails += check_instance(dut.i_verilog.clock, ModifiableObject)
-    fails += check_instance(dut.i_verilog.tx_data, ModifiableObject)
+    fails += check_instance(dut.i_verilog.clock, LogicObject)
+    fails += check_instance(dut.i_verilog.tx_data, LogicObject)
 
     assert fails == 0
diff --git a/tests/test_cases/test_vhdl_access/test_vhdl_access.py b/tests/test_cases/test_vhdl_access/test_vhdl_access.py
--- a/tests/test_cases/test_vhdl_access/test_vhdl_access.py
+++ b/tests/test_cases/test_vhdl_access/test_vhdl_access.py
@@ -24,16 +24,10 @@
 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import logging
-import warnings
 
 import cocotb
-from cocotb.handle import (
-    ConstantObject,
-    EnumObject,
-    HierarchyObject,
-    IntegerObject,
-    ModifiableObject,
-)
+import pytest
+from cocotb.handle import EnumObject, HierarchyObject, IntegerObject, LogicObject
 
 
 # GHDL discovers enum as `vpiNet` (gh-2600)
@@ -56,83 +50,48 @@ async def check_objects(dut):
     Check the types of objects that are returned
     """
     tlog = logging.getLogger("cocotb.test")
-    fails = 0
 
     def check_instance(obj, objtype):
-        if not isinstance(obj, objtype):
-            tlog.error(
-                "Expected {} to be of type {} but got {}".format(
-                    obj._fullname, objtype.__name__, type(obj).__name__
-                )
-            )
-            return 1
+        assert isinstance(
+            obj, objtype
+        ), "Expected {} to be of type {} but got {}".format(
+            obj._fullname, objtype.__name__, type(obj).__name__
+        )
         tlog.info(f"{obj._fullname} is {type(obj).__name__}")
-        return 0
 
     # Hierarchy checks
-    fails += check_instance(dut.inst_axi4s_buffer, HierarchyObject)
-    fails += check_instance(dut.gen_branch_distance[0], HierarchyObject)
-    fails += check_instance(
-        dut.gen_branch_distance[0].inst_branch_distance, HierarchyObject
-    )
-    fails += check_instance(dut.gen_acs[0].inbranch_tdata_low, ModifiableObject)
-    fails += check_instance(dut.gen_acs[0].inbranch_tdata_low[0], ModifiableObject)
-    fails += check_instance(dut.aclk, ModifiableObject)
-    fails += check_instance(dut.s_axis_input_tdata, ModifiableObject)
-    fails += check_instance(dut.current_active, IntegerObject)
-    fails += check_instance(dut.inst_axi4s_buffer.DATA_WIDTH, ConstantObject)
-    fails += check_instance(dut.inst_ram_ctrl, HierarchyObject)
-
-    if dut.inst_axi4s_buffer.DATA_WIDTH != 32:
-        tlog.error(
-            "Expected dut.inst_axi4s_buffer.DATA_WIDTH to be 32 but got %d",
-            dut.inst_axi4s_buffer.DATA_WIDTH,
-        )
-        fails += 1
-
-    try:
+    check_instance(dut.inst_axi4s_buffer, HierarchyObject)
+    check_instance(dut.gen_branch_distance[0], HierarchyObject)
+    check_instance(dut.gen_branch_distance[0].inst_branch_distance, HierarchyObject)
+    check_instance(dut.gen_acs[0].inbranch_tdata_low, LogicObject)
+    check_instance(dut.gen_acs[0].inbranch_tdata_low[0], LogicObject)
+    check_instance(dut.aclk, LogicObject)
+    check_instance(dut.s_axis_input_tdata, LogicObject)
+    check_instance(dut.current_active, IntegerObject)
+    check_instance(dut.inst_axi4s_buffer.DATA_WIDTH, IntegerObject)
+    check_instance(dut.inst_ram_ctrl, HierarchyObject)
+
+    assert (
+        dut.inst_axi4s_buffer.DATA_WIDTH.value == 32
+    ), f"Expected dut.inst_axi4s_buffer.DATA_WIDTH to be 32 but got {dut.inst_axi4s_buffer.DATA_WIDTH.value}"
+
+    with pytest.raises(TypeError):
         dut.inst_axi4s_buffer.DATA_WIDTH.value = 42
-        tlog.error("Shouldn't be allowed to set a value on constant object")
-        fails += 1
-    except TypeError:
-        pass
-
-    try:
-        with warnings.catch_warnings():
-            # The <= syntax is deprecated and raises a DeprecationWarning, which
-            # we need to ignore to get to the condition we actually want to
-            # test.
-            warnings.filterwarnings("ignore", category=DeprecationWarning)
-
-            dut.inst_axi4s_buffer.DATA_WIDTH <= 42
-        tlog.error(
-            "Shouldn't be allowed to set a value on constant object using __le__"
-        )
-        fails += 1
-    except TypeError:
-        pass
-
-    assert fails == 0
 
 
 @cocotb.test()
 async def port_not_hierarchy(dut):
     """
     Test for issue raised by Luke - iteration causes a toplevel port type to
-    change from ModifiableObject to HierarchyObject
+    change from LogicObject to HierarchyObject
     """
-    tlog = logging.getLogger("cocotb.test")
-    if not isinstance(dut.aclk, ModifiableObject):
-        tlog.error(
-            "dut.aclk should be ModifiableObject but got %s", type(dut.aclk).__name__
-        )
-    else:
-        tlog.info("dut.aclk is ModifiableObject")
+    assert isinstance(
+        dut.aclk, LogicObject
+    ), f"dut.aclk should be LogicObject but got {type(dut.aclk).__name__}"
+
     for _ in dut:
         pass
-    if not isinstance(dut.aclk, ModifiableObject):
-        tlog.error(
-            "dut.aclk should be ModifiableObject but got %s", type(dut.aclk).__name__
-        )
-    else:
-        tlog.info("dut.aclk is ModifiableObject")
+
+    assert isinstance(
+        dut.aclk, LogicObject
+    ), f"dut.aclk should be LogicObject but got {type(dut.aclk).__name__}"
diff --git a/tests/test_cases/test_vhdl_zerovector/test_vhdl_zerovector.py b/tests/test_cases/test_vhdl_zerovector/test_vhdl_zerovector.py
--- a/tests/test_cases/test_vhdl_zerovector/test_vhdl_zerovector.py
+++ b/tests/test_cases/test_vhdl_zerovector/test_vhdl_zerovector.py
@@ -19,7 +19,7 @@ async def test_long_signal(dut):
     """Write and read a normal signal (longer than 0)."""
     dut.data_in.value = 0x5
     await Timer(1, "ns")
-    assert dut.data_out == 0x5, "Failed to readback dut.data_out"
+    assert dut.data_out.value == 0x5, "Failed to readback dut.data_out"
 
 
 @cocotb.test(
@@ -32,7 +32,7 @@ async def test_long_signal(dut):
 )
 async def test_read_zero_signal(dut):
     """Read a zero vector. It should always read 0."""
-    assert dut.Cntrl_out == 0, "Failed to readback dut.Cntrl_out"
+    assert dut.Cntrl_out.value == 0, "Failed to readback dut.Cntrl_out"
 
 
 @cocotb.test(
@@ -47,7 +47,7 @@ async def test_write_zero_signal_with_0(dut):
     """Write a zero vector with 0."""
     dut.Cntrl_out.value = 0x0
     await Timer(1, "ns")
-    assert dut.Cntrl_out == 0, "Failed to readback dut.Cntrl_out"
+    assert dut.Cntrl_out.value == 0, "Failed to readback dut.Cntrl_out"
 
 
 @cocotb.test(