Repository: cocotb/cocotb

Issue:
Add new docs / type checking minimum Python version
This Python version will be the minimum supported version for generating docs or doing type checking. Instead of using `if TYPE_CHECKING:` guards in many places, `if sys.version_info >= MIN_TYPING_VERSION` will be used instead. This is necessary because Sphinx does not set `TYPE_CHECKING` to `True` and so all imports and type definitions in those blocks will not resolve correctly.

~~A reasonable minimum Python version for documentation generation and type checking is probably 3.10. This adds `ParamSpec`, `TypeAlias`, and the new typing union syntax `A | B`. Features added in 3.11 aren't important to use.~~ We are currently using `Self` which is a 3.11 feature. Features added in 3.12 are nice but can't be used effectively by us in `if TYPE_CHECKING` blocks. And 3.13 is just far too new to force as a minimum requirement.

Hints from issue comments:

Solution patch:
diff --git a/docs/source/building.rst b/docs/source/building.rst
index a1845c3d..761c8d1d 100644
--- a/docs/source/building.rst
+++ b/docs/source/building.rst
@@ -170,21 +170,26 @@ Cocotb
     Defines how to resolve bits with a value of ``X``, ``Z``, ``U``, ``W``, or ``-`` when being converted to integer.
     Valid settings are:
 
-    ``VALUE_ERROR``
-       Raise a :exc:`ValueError` exception.
-    ``ZEROS``
-       Resolve to ``0``.
-    ``ONES``
-       Resolve to ``1``.
-    ``RANDOM``
-       Randomly resolve to a ``0`` or a ``1``.
-
-    Set to ``VALUE_ERROR`` by default.
+    ``error``
+        Resolves nothing.
+    ``weak``
+        Resolves ``L`` to ``0`` and ``H`` to ``1``.
+    ``zeros``
+        Like ``weak``, but resolves all other non-\ ``0``\ /\ ``1`` values to ``0``.
+    ``ones``
+        Like ``weak``, but resolves all other non-\ ``0``\ /\ ``1`` values to ``1``.
+    ``random``
+        Like ``weak``, but resolves all other non-\ ``0``\ /\ ``1`` values randomly to either ``0`` or ``1``.
+
+    There is also a slight difference in behavior of ``bool(logic)`` when this environment variable is set.
+    When this variable is set, ``bool(logic)`` treats all non-\ ``0``\ /\ ``1`` values as equivalent to ``0``.
+    When this variable is *not* set, ``bool(logic)`` will fail on non-\ ``0``\ /\ ``1`` values.
 
     .. warning::
+        Using this feature is *not* recommended.
 
-        This exists for backwards-compatability reasons.
-        Using any value besides ``VALUE_ERROR`` is *not* recommended.
+    .. deprecated:: 2.0
+        The previously accepted values ``VALUE_ERROR``, ``ZEROS``, ``ONES``, and ``RANDOM`` are deprecated.
 
 .. envvar:: LIBPYTHON_LOC
 
diff --git a/docs/source/library_reference.rst b/docs/source/library_reference.rst
index d1e61e3e..3f1a7a4d 100644
--- a/docs/source/library_reference.rst
+++ b/docs/source/library_reference.rst
@@ -137,11 +137,6 @@ These are a set of datatypes that model the behavior of common HDL datatypes.
     :members:
     :inherited-members:
 
-.. autoclass:: cocotb.types.logic_array.ResolveX
-
-.. autodata:: cocotb.types.logic_array.RESOLVE_X
-
-
 .. _triggers::
 
 Triggers
diff --git a/docs/source/newsfragments/4622.feature.1.rst b/docs/source/newsfragments/4622.feature.1.rst
new file mode 100644
index 00000000..4f48f9e2
--- /dev/null
+++ b/docs/source/newsfragments/4622.feature.1.rst
@@ -0,0 +1 @@
+Added :meth:`.Logic.resolve` and :meth:`.LogicArray.resolve` to resolve non-``0``/``1`` values on demand.
diff --git a/docs/source/newsfragments/4622.feature.rst b/docs/source/newsfragments/4622.feature.rst
new file mode 100644
index 00000000..fd9cf19e
--- /dev/null
+++ b/docs/source/newsfragments/4622.feature.rst
@@ -0,0 +1 @@
+:class:`.LogicArray` now supports :class:`bool` casts and usage in conditionals (e.g. ``if dut.data.value: ...``).
diff --git a/docs/source/newsfragments/4675.bugfix.rst b/docs/source/newsfragments/4675.bugfix.rst
new file mode 100644
index 00000000..c5788b5e
--- /dev/null
+++ b/docs/source/newsfragments/4675.bugfix.rst
@@ -0,0 +1 @@
+Fixed bug where calling :meth:`.Event.wait` but not ``await``\ ing the returned Trigger, then calling :meth:`.Event.set`, then ``await``\ ing the Trigger would hang.
diff --git a/src/cocotb/_base_triggers.py b/src/cocotb/_base_triggers.py
index 8f89a7b3..79a8fa79 100644
--- a/src/cocotb/_base_triggers.py
+++ b/src/cocotb/_base_triggers.py
@@ -84,25 +84,32 @@ class _Event(Trigger):
     can maintain a unique mapping of triggers to tasks.
     """
 
+    _callback: Callable[["_Event"], None]
+
     def __init__(self, parent: "Event") -> None:
         super().__init__()
         self._parent = parent
 
     def _prime(self, callback: Callable[["_Event"], None]) -> None:
         if self._primed:
-            raise RuntimeError(
-                "Event.wait() result can only be used by one task at a time"
-            )
+            return
+        if self._parent.is_set():
+            # If the event is already set, we need to call the callback
+            # immediately, so we don't need to wait for the scheduler.
+            callback(self)
+            return
         self._callback = callback
-        self._parent._prime_trigger(self, callback)
         return super()._prime(callback)
 
     def _unprime(self) -> None:
         if not self._primed:
             return
-        self._parent._unprime_trigger(self)
         return super()._unprime()
 
+    def _set(self) -> None:
+        if self._primed:
+            self._callback(self)
+
     def __repr__(self) -> str:
         return f"<{self._parent!r}.wait() at {pointer_str(self)}>"
 
@@ -140,7 +147,7 @@ class Event:
     """
 
     def __init__(self, name: Optional[str] = None) -> None:
-        self._pending_events: List[_Event] = []
+        self._event: _Event = _Event(self)
         self._name: Union[str, None] = None
         if name is not None:
             self.name = name
@@ -178,14 +185,6 @@ class Event:
     def data(self, new_data: Any) -> None:
         self._data = new_data
 
-    def _prime_trigger(
-        self, trigger: _Event, callback: Callable[[_Event], None]
-    ) -> None:
-        self._pending_events.append(trigger)
-
-    def _unprime_trigger(self, trigger: _Event) -> None:
-        self._pending_events.remove(trigger)
-
     def set(self, data: Optional[Any] = None) -> None:
         """Set the Event and unblock all Tasks blocked on this Event."""
         self._fired = True
@@ -195,10 +194,7 @@ class Event:
                 DeprecationWarning,
             )
         self._data = data
-
-        pending_events, self._pending_events = self._pending_events, []
-        for event in pending_events:
-            event._callback(event)
+        self._event._set()
 
     def wait(self) -> Trigger:
         """Block the current Task until the Event is set.
@@ -209,9 +205,7 @@ class Event:
         To reset the Event (and enable the use of :meth:`wait` again),
         call :meth:`clear`.
         """
-        if self._fired:
-            return NullTrigger(name=f"{str(self)}.wait()")
-        return _Event(self)
+        return self._event
 
     def clear(self) -> None:
         """Clear this event that has been set.
diff --git a/src/cocotb/_py_compat.py b/src/cocotb/_py_compat.py
index 1bc3b3ef..a3ab7bda 100644
--- a/src/cocotb/_py_compat.py
+++ b/src/cocotb/_py_compat.py
@@ -12,7 +12,11 @@ import sys
 from contextlib import AbstractContextManager
 from typing import TypeVar, Union, overload
 
-__all__ = ("StrEnum", "cached_property", "insertion_ordered_dict", "nullcontext")
+__all__ = (
+    "cached_property",
+    "insertion_ordered_dict",
+    "nullcontext",
+)
 
 T = TypeVar("T")
 
@@ -101,13 +105,3 @@ else:
             res = self._method(instance)
             instance.__dict__[self._method.__name__] = res
             return res
-
-
-# inheriting from (str, Enum) was broken in 3.11 and StrEnum must be used
-if sys.version_info >= (3, 11):
-    from enum import StrEnum
-else:
-    from enum import Enum
-
-    class StrEnum(str, Enum):
-        pass
diff --git a/src/cocotb/_utils.py b/src/cocotb/_utils.py
index d27698eb..cd4bdba9 100644
--- a/src/cocotb/_utils.py
+++ b/src/cocotb/_utils.py
@@ -27,8 +27,6 @@ from typing import (
     overload,
 )
 
-from cocotb._py_compat import StrEnum
-
 
 @lru_cache(maxsize=None)
 def want_color_output() -> bool:
@@ -205,20 +203,6 @@ class DocIntEnum(IntEnum):
         return self
 
 
-StrEnumT = TypeVar("StrEnumT", bound=StrEnum)
-
-
-class DocStrEnum(StrEnum):
-    """Like DocEnum but for StrEnum enum types."""
-
-    def __new__(cls: Type[StrEnumT], value: str, doc: Optional[str] = None) -> StrEnumT:
-        self = str.__new__(cls, value)
-        self._value_ = value
-        if doc is not None:
-            self.__doc__ = doc
-        return self
-
-
 if TYPE_CHECKING:
     F = TypeVar("F")
 
diff --git a/src/cocotb/types/_logic.py b/src/cocotb/types/_logic.py
index 7d2fbe71..0443add8 100644
--- a/src/cocotb/types/_logic.py
+++ b/src/cocotb/types/_logic.py
@@ -10,6 +10,8 @@ from typing import (
     Union,
 )
 
+from cocotb.types._resolve import RESOLVE_X, ResolverLiteral, get_str_resolver
+
 LogicLiteralT = Union[str, int, bool]
 LogicConstructibleT = Union[LogicLiteralT, "Logic"]
 
@@ -228,19 +230,64 @@ class Logic:
     def __str__(self) -> str:
         return ("U", "X", "0", "1", "Z", "W", "L", "H", "-")[self._repr]
 
-    def __bool__(self) -> bool:
-        if self._repr in (_0, _L):
+    if RESOLVE_X is None:
+
+        def __bool__(self) -> bool:
+            if self._repr in (_0, _L):
+                return False
+            elif self._repr in (_1, _H):
+                return True
+            raise ValueError(f"Cannot convert {self!r} to bool")
+
+        def __int__(self) -> int:
+            if self._repr in (_0, _L):
+                return 0
+            elif self._repr in (_1, _H):
+                return 1
+            raise ValueError(f"Cannot convert {self!r} to int")
+
+    else:
+
+        def __bool__(self) -> bool:
+            if self._repr in (_1, _H):
+                return True
             return False
-        elif self._repr in (_1, _H):
-            return True
-        raise ValueError(f"Cannot convert {self!r} to bool")
 
-    def __int__(self) -> int:
-        if self._repr in (_0, _L):
-            return 0
-        elif self._repr in (_1, _H):
-            return 1
-        raise ValueError(f"Cannot convert {self!r} to int")
+        def __int__(self) -> int:
+            s = str(self)
+            s = RESOLVE_X(s)  # type: ignore  # mypy fails to narrow RESOLVE_X here
+            return int(s, 2)
 
     def __index__(self) -> int:
         return int(self)
+
+    def resolve(self, resolver: ResolverLiteral) -> "Logic":
+        """Resolves non-0/1 values to 0/1.
+
+        The possible values of the *resolver* argument are:
+
+        * ``"weak"``: Weak values are resolved to their strong-valued equivalents.
+
+        * ``"zeros"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are resolved to ``0``.
+
+        * ``"ones"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are resolved to ``1``.
+
+        * ``"random"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are randomly resolved to either ``0`` or ``1``.
+
+        Args:
+            resolver: How to resolve non-``0``/``1`` values. See possible values above.
+
+        Returns:
+            The resolved Logic.
+
+        Raises:
+            ValueError: Invalid *resolver* value.
+            TypeError: Unsupported *value* type.
+        """
+        return Logic(get_str_resolver(resolver)(str(self)))
diff --git a/src/cocotb/types/_logic_array.py b/src/cocotb/types/_logic_array.py
index a5f36a49..c7ac8f9a 100644
--- a/src/cocotb/types/_logic_array.py
+++ b/src/cocotb/types/_logic_array.py
@@ -1,12 +1,9 @@
 # Copyright cocotb contributors
 # Licensed under the Revised BSD License, see LICENSE for details.
 # SPDX-License-Identifier: BSD-3-Clause
-import os
-import random
 import sys
 from math import ceil
 from typing import (
-    Dict,
     Iterable,
     Iterator,
     List,
@@ -16,104 +13,48 @@ from typing import (
 )
 
 from cocotb._deprecation import deprecated
-from cocotb._utils import DocStrEnum
 from cocotb.types._abstract_array import AbstractArray
 from cocotb.types._logic import Logic, LogicConstructibleT, _str_literals
 from cocotb.types._range import Range
+from cocotb.types._resolve import RESOLVE_X, ResolverLiteral, get_str_resolver
 
 if sys.version_info >= (3, 8):
     from typing import Literal
 
 
-class ResolveX(DocStrEnum):
-    """Resolution behaviors supported when converting a :class:`LogicArray` to an integer.
-
-    The values ``L`` and ``H`` are always resolved to ``0`` and ``1`` respectively.
-    These behaviors exist to resolve the Logic values ``X``, ``Z``, ``U``, ``W``, and ``-``
-    to either ``0`` or ``1``.
-    """
-
-    VALUE_ERROR = (
-        "error",
-        "Throws a :exc:`ValueError` if the :class:`LogicArray` contains non-``0``/``1`` values.",
-    )
-    ZEROS = ("zeros", "Resolves all non-``0``/``1`` values to ``0``.")
-    ONES = ("ones", "Resolves all non-``0``/``1`` values to ``1``.")
-    RANDOM = (
-        "random",
-        "Resolves all non-``0``/``1`` values randomly to either ``0`` or ``1``.",
-    )
-
-
-RESOLVE_X = ResolveX[os.getenv("COCOTB_RESOLVE_X", "VALUE_ERROR")]
-"""Global default for resolving ``X``, ``Z``, ``U``, ``W``, and ``-`` values to ``0`` or ``1``.
-
-Set using :envvar:`COCOTB_RESOLVE_X` before boot, or via this variable any time thereafter.
-Defaults to :attr:`~ResolveX.VALUE_ERROR`.
-
-.. warning::
-
-    This exists for backwards-compatibility reasons.
-    Using any value besides ``VALUE_ERROR`` is *not* recommended.
-"""
-
-
 _resolve_lh_table = str.maketrans({"L": "0", "H": "1"})
 
-_ord_0 = ord("0")
-
-
-class _error_resolve_table(Dict[int, int]):
-    def __init__(self) -> None:
-        self.update({ord(c): ord(c) for c in "01"})
-
-    def __missing__(self, key: str) -> int:
-        raise ValueError(f"Unresolvable bit in binary string: {key!r}.")
-
-
-class _random_resolve_table(Dict[int, int]):
-    def __init__(self) -> None:
-        self.update({ord(c): ord(c) for c in "01"})
-
-    def __missing__(self, _: str) -> int:
-        return random.getrandbits(1) + _ord_0
-
-
-_resolve_tables = {
-    "error": _error_resolve_table(),
-    "zeros": str.maketrans("XZUW-", "00000"),
-    "ones": str.maketrans("XZUW-", "11111"),
-    "random": _random_resolve_table(),
-}
-
 
 class LogicArray(AbstractArray[Logic]):
-    r"""Fixed-sized, arbitrarily-indexed, array of :class:`cocotb.types.Logic`.
+    r"""Fixed-sized, arbitrarily-indexed, Array of Logics.
 
     .. currentmodule:: cocotb.types
 
-    :class:`LogicArray`\ s can be constructed from iterables of values
-    constructible into :class:`Logic` like :class:`bool`, :class:`str`, :class:`int`,
-    or it can be constructed :class:`str` or :class:`int` literals syntaxes, as seen below.
+    An :class:`Array`, where all elements are enforced to be :class:`Logic`.
+    This allows the additional of bit-wise logical operators, conversions to integers and bytes, and ``X`` testing and mapping.
 
-    Like :class:`Array`, if *range* is not given, the range ``Range(len(value)-1, "downto", 0)`` is used.
-    If an :class:`int` is passed for *range*, the range ``Range(range-1, "downto", 0)`` is used.
+    :class:`!LogicArray`\ s can be constructed from an iterable of :class:`!Logic`\ s,
+    or values constructible into :class:`!Logic`, like :class:`bool`, :class:`str`, or :class:`int`.
+    Alternatively, they can be constructed from :class:`!str` or :class:`!int` literals.
+
+    Like :class:`Array`, if *range* is not given, the range ``Range(len(value)-1, "downto", 0)`` is used;
+    and if an :class:`int` is passed for *range*, the range ``Range(range-1, "downto", 0)`` is used.
 
     .. code-block:: pycon3
 
         >>> LogicArray(0b0111, 4)
         LogicArray('0111', Range(3, 'downto', 0))
 
-        >>> LogicArray("01XZ")
-        LogicArray('01XZ', Range(3, 'downto', 0))
+        >>> LogicArray("01XZ", Range(0, "to", 3))
+        LogicArray('01XZ', Range(0, 'to', 3))
 
-        >>> LogicArray([0, True, "X"])
-        LogicArray('01X', Range(2, 'downto', 0))
+        >>> LogicArray([0, True, "X", Logic("-")])
+        LogicArray('01X-', Range(3, 'downto', 0))
 
     .. note::
-        If constructing from an unsigned :class:`int` literal, *range* `must` be given.
+        If constructing from an unsigned :class:`!int` literal, *range* `must` be given.
 
-    :class:`LogicArray`\ s can be constructed from :class:`int`\ s using :meth:`from_unsigned` or :meth:`from_signed`.
+    :class:`!LogicArray`\ s can be constructed from :class:`int`\ s using :meth:`from_unsigned` or :meth:`from_signed`.
 
     .. code-block:: pycon3
 
@@ -123,7 +64,7 @@ class LogicArray(AbstractArray[Logic]):
         >>> LogicArray.from_signed(-4, Range(0, "to", 3))  # will sign-extend
         LogicArray('1100', Range(0, 'to', 3))
 
-    :class:`LogicArray`\ s can be constructed from :class:`bytes` or :class:`bytearray` using :meth:`from_bytes`.
+    :class:`!LogicArray`\ s can be constructed from :class:`bytes` or :class:`bytearray` using :meth:`from_bytes`.
     Use the *byteorder* argument to control endianness.
 
     .. code-block:: pycon3
@@ -134,87 +75,94 @@ class LogicArray(AbstractArray[Logic]):
         >>> LogicArray.from_bytes(b"1n", byteorder="little")
         LogicArray('0110111000110001', Range(15, 'downto', 0))
 
-    :class:`LogicArray`\ s support the same :class:`list`-like operations as :class:`Array`;
-    however, it enforces the condition that all elements must be a :class:`Logic`.
+    :class:`!LogicArray`\ s support the same :class:`list`-like operations as :class:`Array`;
+    however, it enforces the condition that all elements must be a :class:`!Logic`.
 
     .. code-block:: pycon3
 
-        >>> la = LogicArray("1010")
-        >>> la[0]  # is indexable
+        >>> array = LogicArray("1010")
+        >>> array[0]  # is indexable
         Logic('0')
 
-        >>> la[1:]  # is slice-able
+        >>> array[1:]  # is slice-able
         LogicArray('10', Range(1, 'downto', 0))
 
-        >>> Logic("0") in la  # is a collection
+        >>> Logic("0") in array  # is a collection
         True
 
-        >>> list(la)  # is an iterable
+        >>> list(array)  # 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`.
+    When setting an element or slice, the *value* is first constructed into a :class:`Logic`.
 
     .. code-block:: pycon3
 
-        >>> la = LogicArray("1010")
-        >>> la[3] = "Z"
-        >>> la[3]
+        >>> array = LogicArray("1010")
+        >>> array[3] = "Z"
+        >>> array[3]
         Logic('Z')
 
-        >>> la[2:] = ["X", True, 0]
-        >>> la
+        >>> array[2:] = ["X", True, 0]
+        >>> array
         LogicArray('ZX10', Range(3, 'downto', 0))
 
-        >>> la[:] = 0b0101
-        >>> la
+        >>> array[:] = 0b0101
+        >>> array
         LogicArray('0101', Range(3, 'downto', 0))
 
-    :class:`LogicArray`\ s can be converted into their :class:`str` or :class:`int` literal values using casts.
+    :class:`!LogicArray`\ s can be converted into their :class:`str` or :class:`int` literal values using casts.
+    They can also be used in conditionals.
 
     .. code-block:: pycon3
 
-        >>> la = LogicArray("1010")
-        >>> str(la)
+        >>> value = LogicArray("1010")
+        >>> str(value)
         '1010'
-        >>> int(la)
+
+        >>> int(value)
         10
+        >>> if value:
+        ...     print("Not 0!")
+        Not 0!
 
     .. warning::
-        The :class:`int` cast assumes the value is entirely ``0`` or ``1`` and will raise an exception otherwise.
+        The :class:`int` cast, :class:`bool` cast, and use in conditionals assumes the
+        value is entirely ``0``, ``1``, ``L``, or ``H``, and will raise an exception otherwise.
 
     The :meth:`to_unsigned`, :meth:`to_signed`, and :meth:`to_bytes` methods can be used to convert
     the value into an unsigned or signed integer, or bytes, respectively.
 
     .. code-block:: pycon3
 
-        >>> la.to_unsigned()
+        >>> value = LogicArray("1010")
+        >>> value.to_unsigned()
         10
 
-        >>> la.to_signed()
+        >>> value.to_signed()
         -6
 
-        >>> la.to_bytes(byteorder="big")
+        >>> value.to_bytes(byteorder="big")
         b'\n'
 
     .. warning::
-        These operations assume the value is entirely ``0`` or ``1`` and will raise an exception otherwise.
+        These operations assume the value is entirely ``0``, ``1``, ``L``, or ``H``, and will raise an exception otherwise.
 
     You can also convert :class:`LogicArray`\ s to hexadecimal or binary strings using
     the built-ins :func:`hex:` and :func:`bin`, respectively.
 
     .. code-block:: pycon3
 
-        >>> la = LogicArray("01111010")
-        >>> hex(la)
+        >>> value = LogicArray("01111010")
+        >>> hex(value)
         '0x7a'
-        >>> bin(la)
+
+        >>> bin(value)
         '0b1111010'
 
     .. warning::
         Using :func:`hex` or :func:`bin` first turns the LogicArray into an :class:`int`.
         This means the exact length of the LogicArray is lost.
-        It also means that these expressions will raise an exception if the value is not entirely ``0`` or ``1``.
+        It also means that these expressions will raise an exception if the value is not entirely ``0``, ``1``, ``L``, or ``H``.
 
     :class:`LogicArray`\ s also support element-wise logical operations: ``&``, ``|``,
     ``^``, and ``~``.
@@ -225,10 +173,10 @@ class LogicArray(AbstractArray[Logic]):
         ...     s = LogicArray([sel] * len(a))
         ...     return (a & ~s) | (b & s)
 
-        >>> la = LogicArray("0110")
-        >>> p = LogicArray("1110")
+        >>> a = LogicArray("0110")
+        >>> b = LogicArray("1110")
         >>> sel = Logic("1")  # choose second option
-        >>> big_mux(la, p, sel)
+        >>> big_mux(a, b, sel)
         LogicArray('1110', Range(3, 'downto', 0))
 
     Args:
@@ -317,10 +265,7 @@ class LogicArray(AbstractArray[Logic]):
                 )
         return self._value_as_str
 
-    def _get_int(
-        self,
-        resolve: "ResolveX | Literal['error'] | Literal['zeros'] | Literal['ones'] | Literal['random'] | None",
-    ) -> int:
+    def _get_int(self) -> int:
         if self._value_as_int is None:
             # May convert list to str before converting to int.
             value_as_str = self._get_str()
@@ -331,14 +276,13 @@ class LogicArray(AbstractArray[Logic]):
             try:
                 self._value_as_int = int(value_as_str, 2)
             except ValueError:
-                # value needs resolving
-                if resolve is None:
-                    resolve = RESOLVE_X
-
-                # resolve remaining
-                resolve_table = _resolve_tables[resolve]
-                value_as_str = value_as_str.translate(resolve_table)
-                return int(value_as_str, 2)
+                if RESOLVE_X is None:
+                    raise ValueError(
+                        f"Can't convert {type(self).__qualname__} to int: it contains non-0/1 values"
+                    ) from None
+                else:
+                    value_as_str = RESOLVE_X(value_as_str)
+                    return int(value_as_str, 2)
 
         return self._value_as_int
 
@@ -605,51 +549,33 @@ class LogicArray(AbstractArray[Logic]):
         """
         return self.to_bytes(byteorder="big")
 
-    def to_unsigned(
-        self,
-        resolve: "ResolveX | Literal['error'] | Literal['zeros'] | Literal['ones'] | Literal['random'] | None" = None,
-    ) -> int:
+    def to_unsigned(self) -> int:
         """Convert the value to an integer by interpreting it using unsigned representation.
 
         The :class:`LogicArray` is treated as an arbitrary-length vector of bits
         with the left-most bit being the most significant bit in the integer value.
         The bit vector is then interpreted as an integer using unsigned representation.
 
-        Args:
-            resolve:
-                How ``X``, ``Z``, ``U``, ``W``, and ``-`` values should be resolve to ``0`` or ``1`` to allow conversion to an integer.
-                See :class:`ResolveX` for details.
-                Defaults to the current value of :attr:`~cocotb.types.logic_array.RESOLVE_X`.
-
         Returns:
             An integer equivalent to the value by interpreting it using unsigned representation.
         """
         if len(self) == 0:
             raise ValueError("Cannot convert null vector to integer")
-        return self._get_int(resolve)
+        return self._get_int()
 
-    def to_signed(
-        self,
-        resolve: "ResolveX | Literal['error'] | Literal['zeros'] | Literal['ones'] | Literal['random'] | None" = None,
-    ) -> int:
+    def to_signed(self) -> int:
         """Convert the value to an integer by interpreting it using two's complement representation.
 
         The :class:`LogicArray` is treated as an arbitrary-length vector of bits
         with the left-most bit being the most significant bit in the integer value.
         The bit vector is then interpreted as an integer using two's complement representation.
 
-        Args:
-            resolve:
-                How ``X``, ``Z``, ``U``, ``W``, and ``-`` values should be resolve to ``0`` or ``1`` to allow conversion to an integer.
-                See :class:`ResolveX` for details.
-                Defaults to the current value of :attr:`~cocotb.types.logic_array.RESOLVE_X`.
-
         Returns:
             An integer equivalent to the value by interpreting it using two's complement representation.
         """
         if len(self) == 0:
             raise ValueError("Cannot convert null vector to integer")
-        value = self._get_int(resolve)
+        value = self._get_int()
         limit = 1 << (len(self) - 1)
         if value >= limit:
             value -= 2 * limit
@@ -794,9 +720,47 @@ class LogicArray(AbstractArray[Logic]):
     def __invert__(self) -> "LogicArray":
         return LogicArray(~v for v in self)
 
-    @deprecated(
-        "`bool()` casts and using LogicArray in conditional expressions is deprecated. "
-        """Use explicit comparisons (e.g. `LogicArray() == "11"`) or `all`/`any` expressions instead."""
-    )
-    def __bool__(self) -> bool:
-        return any(v in (Logic("H"), Logic("1")) for v in self)
+    if RESOLVE_X is None:
+
+        def __bool__(self) -> bool:
+            if len(self) == 0:
+                return False
+            return bool(int(self))
+
+    else:
+
+        def __bool__(self) -> bool:
+            if len(self) == 0:
+                return False
+            return any(bool(bit) for bit in self)
+
+    def resolve(self, resolver: ResolverLiteral) -> "LogicArray":
+        """Resolves non-0/1 values to 0/1.
+
+        The possible values of the *resolver* argument are:
+
+        * ``"weak"``: Weak values are resolved to their strong-valued equivalents.
+
+        * ``"zeros"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are resolved to ``0``.
+
+        * ``"ones"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are resolved to ``1``.
+
+        * ``"random"``:
+            ``L`` and ``H`` are resolved to ``0`` and ``1``, respectively.
+            Remaining non-``0``/``1`` values are randomly resolved to either ``0`` or ``1``.
+
+        Args:
+            resolver: How to resolve non-``0``/``1`` values. See possible values above.
+
+        Returns:
+            The resolved Logic.
+
+        Raises:
+            ValueError: Invalid *resolver* value.
+            TypeError: Unsupported *value* type.
+        """
+        return LogicArray(get_str_resolver(resolver)(str(self)), self.range)
diff --git a/src/cocotb/types/_resolve.py b/src/cocotb/types/_resolve.py
new file mode 100644
index 00000000..8cdb5512
--- /dev/null
+++ b/src/cocotb/types/_resolve.py
@@ -0,0 +1,79 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+import os
+import sys
+from functools import lru_cache
+from random import getrandbits
+from typing import Callable, Dict, Union
+
+if sys.version_info >= (3, 10):
+    from typing import Final, Literal, TypeAlias  # noqa: F401  # used in type strings
+
+
+ResolverLiteral: "TypeAlias" = "Literal['weak', 'zeros', 'ones', 'random']"
+
+
+_ord_0 = ord("0")
+
+
+class _random_resolve_table(Dict[int, int]):
+    def __init__(self) -> None:
+        self[ord("0")] = ord("0")
+        self[ord("1")] = ord("1")
+        self[ord("L")] = ord("0")
+        self[ord("H")] = ord("1")
+
+    def __missing__(self, _: str) -> int:
+        return getrandbits(1) + _ord_0
+
+
+_resolve_tables: Dict[str, Dict[int, int]] = {
+    "error": {},
+    "weak": str.maketrans("LHW", "01X"),
+    "zeros": str.maketrans("LHUXZW-", "0100000"),
+    "ones": str.maketrans("LHUXZW-", "0111111"),
+    "random": _random_resolve_table(),
+}
+
+_VALID_RESOLVERS = ("error", "weak", "zeros", "ones", "random")
+_VALID_RESOLVERS_ERR_MSG = (
+    "Valid values are 'error', 'weak', 'zeros', 'ones', or 'random'"
+)
+
+
+@lru_cache(maxsize=None)
+def get_str_resolver(resolver: ResolverLiteral) -> Callable[[str], str]:
+    if resolver not in _VALID_RESOLVERS:
+        raise ValueError(f"Invalid resolver: {resolver!r}. {_VALID_RESOLVERS_ERR_MSG}")
+
+    resolve_table = _resolve_tables[resolver]
+
+    def resolve_func(value: str) -> str:
+        return value.translate(resolve_table)
+
+    return resolve_func
+
+
+def _init() -> Union[Callable[[str], str], None]:
+    _envvar = os.getenv("COCOTB_RESOLVE_X", None)
+
+    # no resolver
+    if _envvar is None:
+        return None
+
+    # backwards compatibility
+    resolver = _envvar.strip().lower()
+    if resolver == "value_error":
+        resolver = "error"
+
+    # get resolver
+    try:
+        return get_str_resolver(resolver)
+    except ValueError:
+        raise ValueError(
+            f"Invalid COCOTB_RESOLVE_X value: {_envvar!r}. {_VALID_RESOLVERS_ERR_MSG}"
+        ) from None
+
+
+RESOLVE_X: "Final" = _init()


Test patch:
diff --git a/tests/pytest/test_logic.py b/tests/pytest/test_logic.py
index dcd0edd1..c09b7175 100644
--- a/tests/pytest/test_logic.py
+++ b/tests/pytest/test_logic.py
@@ -162,3 +162,24 @@ def test_logic_identity():
     assert Logic("1") is Logic(1)
     assert Logic("X") is Logic("x")
     assert Logic("z") is Logic("Z")
+
+
+def test_resolve():
+    for inp, exp in zip("UX01ZWLH-", "UX01ZX01-"):
+        assert Logic(inp).resolve("weak") == Logic(exp)
+
+    for inp, exp in zip("UX01ZWLH-", "000100010"):
+        assert Logic(inp).resolve("zeros") == Logic(exp)
+
+    for inp, exp in zip("UX01ZWLH-", "110111011"):
+        assert Logic(inp).resolve("ones") == Logic(exp)
+
+    assert Logic("U").resolve("random") in (Logic("0"), Logic("1"))
+    assert Logic("X").resolve("random") in (Logic("0"), Logic("1"))
+    assert Logic("0").resolve("random") == Logic("0")
+    assert Logic("1").resolve("random") == Logic("1")
+    assert Logic("Z").resolve("random") in (Logic("0"), Logic("1"))
+    assert Logic("W").resolve("random") in (Logic("0"), Logic("1"))
+    assert Logic("L").resolve("random") == Logic("0")
+    assert Logic("H").resolve("random") == Logic("1")
+    assert Logic("-").resolve("random") in (Logic("0"), Logic("1"))
diff --git a/tests/pytest/test_logic_array.py b/tests/pytest/test_logic_array.py
index 1ef6e931..4a6fe6dd 100644
--- a/tests/pytest/test_logic_array.py
+++ b/tests/pytest/test_logic_array.py
@@ -1,7 +1,6 @@
 # Copyright cocotb contributors
 # Licensed under the Revised BSD License, see LICENSE for details.
 # SPDX-License-Identifier: BSD-3-Clause
-import warnings
 
 import pytest
 
@@ -404,53 +403,16 @@ def test_null_vector():
 
 
 def test_bool_cast():
-    with pytest.warns(DeprecationWarning):
-        assert LogicArray("0110")
-    with warnings.catch_warnings():
-        warnings.filterwarnings(action="ignore", category=DeprecationWarning)
-        assert not LogicArray("0000")
-        assert LogicArray("01XZ")
-        assert LogicArray("XZ01")
-
-
-def test_resolve_x():
-    a = LogicArray("UX01ZWLH-")
-
-    with pytest.raises(ValueError):
-        a.to_unsigned("error")
-    with pytest.raises(ValueError):
-        a.to_signed("error")
-    assert LogicArray("01LH").to_unsigned("error") == 0b0101
-
-    assert a.to_unsigned("ones") == 0b110111011
-
-    assert a.to_unsigned("zeros") == 0b000100010
-
-    rand_val = a.to_unsigned("random")
-    # check known bits only
-    assert (rand_val >> 1) & 1 == 1
-    assert (rand_val >> 2) & 1 == 0
-    assert (rand_val >> 5) & 1 == 1
-    assert (rand_val >> 6) & 1 == 0
-
-
-def test_resolve_default_behavior():
-    import cocotb.types._logic_array
-
-    a = LogicArray("01X")
-
-    cocotb.types._logic_array.RESOLVE_X = "error"
+    assert not LogicArray("0000")
+    assert LogicArray("0100")
     with pytest.raises(ValueError):
-        a.to_unsigned()
-
-    cocotb.types._logic_array.RESOLVE_X = "zeros"
-    assert a.to_unsigned() == 0b010
+        bool(LogicArray("XZ01"))
 
-    cocotb.types._logic_array.RESOLVE_X = "ones"
-    assert a.to_unsigned() == 0b011
 
-    cocotb.types._logic_array.RESOLVE_X = "random"
-    rand_val = a.to_unsigned()
-    # check known bits only
-    assert (rand_val >> 1) & 1 == 1
-    assert (rand_val >> 2) & 1 == 0
+def test_resolve():
+    assert LogicArray("UX01ZWLH-").resolve("weak") == LogicArray("UX01ZX01-")
+    assert LogicArray("UX01ZWLH-").resolve("zeros") == LogicArray("000100010")
+    assert LogicArray("UX01ZWLH-").resolve("ones") == LogicArray("110111011")
+    assert LogicArray("01LH").resolve("random") == LogicArray("0101")
+    array = LogicArray("UXZW-").resolve("random")
+    assert all(elem in (Logic("0"), Logic("1")) for elem in array)
diff --git a/tests/test_cases/test_cocotb/test_synchronization_primitives.py b/tests/test_cases/test_cocotb/test_synchronization_primitives.py
index 3c2162d2..53c8ae75 100644
--- a/tests/test_cases/test_cocotb/test_synchronization_primitives.py
+++ b/tests/test_cases/test_cocotb/test_synchronization_primitives.py
@@ -7,7 +7,7 @@ Tests for synchronization primitives like Lock and Event
 
 import random
 import re
-from typing import List
+from typing import Any, List
 
 import pytest
 from common import assert_takes
@@ -21,6 +21,7 @@ from cocotb.triggers import (
     NullTrigger,
     ReadOnly,
     Timer,
+    with_timeout,
 )
 
 
@@ -222,7 +223,7 @@ async def test_Lock_multiple_users_acquire_triggers(_) -> None:
     await Timer(1, "ns")
 
 
-@cocotb.test(expect_error=RuntimeError)
+@cocotb.test
 async def test_Event_multiple_task_share_trigger(_) -> None:
     """Test that multiple tasks aren't allowed to share an Event trigger."""
 
@@ -235,3 +236,16 @@ async def test_Event_multiple_task_share_trigger(_) -> None:
     cocotb.start_soon(waiter(e_trigger))
 
     await Timer(1, "ns")
+
+
+@cocotb.test
+async def test_Event_wait_after_set(_: Any) -> None:
+    """Test that getting the _Event Trigger, setting it, then awaiting the Trigger doesn't hang."""
+
+    e = Event()
+    trigger = e.wait()
+
+    e.set()
+
+    # Should not block
+    await with_timeout(trigger, 1, "step")