Repository: cocotb/cocotb

Issue:
Check that `RisingEdge` and `FallingEdge` are only called on 1 bit signals
Warn in 1.8 and throw in 2.0.


Discussion:

Resolving patch:
diff --git a/src/cocotb/triggers.py b/src/cocotb/triggers.py
--- a/src/cocotb/triggers.py
+++ b/src/cocotb/triggers.py
@@ -39,6 +39,7 @@
 import cocotb.task
 from cocotb import outcomes, simulator
 from cocotb._py_compat import cached_property
+from cocotb.handle import LogicObject, ModifiableObject
 from cocotb.log import SimLog
 from cocotb.utils import (
     ParametrizedSingleton,
@@ -365,10 +366,6 @@ def _edge_type(self):
         """The edge type, as understood by the C code. Must be set in sub-classes."""
         raise NotImplementedError
 
-    @classmethod
-    def __singleton_key__(cls, signal):
-        return signal
-
     def __init__(self, signal):
         super().__init__()
         self.signal = signal
@@ -393,6 +390,12 @@ class RisingEdge(_EdgeBase):
     __slots__ = ()
     _edge_type = 1
 
+    @classmethod
+    def __singleton_key__(cls, signal):
+        if not (isinstance(signal, LogicObject) and len(signal) == 1):
+            raise TypeError("")
+        return signal
+
 
 class FallingEdge(_EdgeBase):
     """Fires on the falling edge of *signal*, on a transition from ``1`` to ``0``."""
@@ -400,6 +403,12 @@ class FallingEdge(_EdgeBase):
     __slots__ = ()
     _edge_type = 2
 
+    @classmethod
+    def __singleton_key__(cls, signal):
+        if not (isinstance(signal, LogicObject) and len(signal) == 1):
+            raise TypeError("")
+        return signal
+
 
 class Edge(_EdgeBase):
     """Fires on any value change of *signal*."""
@@ -407,6 +416,12 @@ class Edge(_EdgeBase):
     __slots__ = ()
     _edge_type = 3
 
+    @classmethod
+    def __singleton_key__(cls, signal):
+        if not isinstance(signal, ModifiableObject):
+            raise TypeError("")
+        return signal
+
 
 class _Event(PythonTrigger):
     """Unique instance used by the Event object.


Test patch:
diff --git a/tests/test_cases/test_cocotb/test_edge_triggers.py b/tests/test_cases/test_cocotb/test_edge_triggers.py
--- a/tests/test_cases/test_cocotb/test_edge_triggers.py
+++ b/tests/test_cases/test_cocotb/test_edge_triggers.py
@@ -10,6 +10,7 @@
 * ClockCycles
 """
 import cocotb
+import pytest
 from cocotb.clock import Clock
 from cocotb.result import SimTimeoutError
 from cocotb.triggers import (
@@ -21,6 +22,7 @@
     ReadOnly,
     RisingEdge,
     Timer,
+    with_timeout,
 )
 
 
@@ -284,3 +286,48 @@ async def wait_edge():
         await RisingEdge(dut.clk)
 
     assert edge_cnt == 2 * ((2 ** len(dut.stream_in_data) - 1) - 1)
+
+
+@cocotb.test()
+async def test_edge_bad_handles(dut):
+    with pytest.raises(TypeError):
+        RisingEdge(dut)
+
+    with pytest.raises(TypeError):
+        FallingEdge(dut)
+
+    with pytest.raises(TypeError):
+        Edge(dut)
+
+    with pytest.raises(TypeError):
+        RisingEdge(dut.stream_in_data)
+
+    with pytest.raises(TypeError):
+        FallingEdge(dut.stream_in_data)
+
+
+@cocotb.test()
+async def test_edge_logic_vector(dut):
+    dut.stream_in_data.value = 0
+
+    async def change_stream_in_data():
+        await Timer(10, "ns")
+        dut.stream_in_data.value = 10
+
+    cocotb.start_soon(change_stream_in_data())
+
+    await with_timeout(Edge(dut.stream_in_data), 20, "ns")
+
+
+# icarus doesn't support integer inputs/outputs
+@cocotb.test(skip=cocotb.SIM_NAME.lower().startswith("icarus"))
+async def test_edge_non_logic_handles(dut):
+    dut.stream_in_int.value = 0
+
+    async def change_stream_in_int():
+        await Timer(10, "ns")
+        dut.stream_in_int.value = 10
+
+    cocotb.start_soon(change_stream_in_int())
+
+    await with_timeout(Edge(dut.stream_in_int), 20, "ns")