Repository: cocotb/cocotb

Issue:
32-bit signals are driven by binary string and not the integer value
In handle.py, the function setimmediatevalue() will check if the value is an integer and then check if the integer is less than 0x7fffffff and then it will drive the simulator quicker if it is.  It seems like any 32-bit value should be able to be driven in the same manner.  I realize that that if the value uses the most significant bit, then the C-function, will treat that value as a negative integer, but from the code I have looked at, there wouldn't be any impact to the way the signal is driven.

@chiggs Is there a reason that any 32-bit and below values don't use the faster interface, instead of 31-bit and below?  It seems like the C-Interface would support this.



Discussion:
It likely stems from the VPI definition of a value, which by the specification is an `int32_t`.

It would be difficult to prove that all simulators treated this as `uint32_t`, however VHPI uses `uint32_t` and FLI arguably doesn't matter - we should even default to `uint64_t` before switching to strings.

It would be nice if in Python, we determined the threshold for switching to string-based slow path when the signal object is discovered, based on some constant we could read from the interface.

We can do that with a capabilities method that python can call. Would be extensible and we can have some sensible defaults that are inplementation dependent. 

The VPI definition of int32_t comes most likely from the fact that int/integer in SystemVerilog are defined as signed 32 bit type. So the `IntegerObject` should at least support the negative range as well.

However, as far as I could see, the `unsigned` qualifier is not reflected in VPI; both `logic int unsigned something` and `logic int something` are most likely mapped to `vpiIntegerNet`. This means we can safely cast uint32 to int32 and set it through `simulator.set_signal_val_long()`.

Needs more research, but could be a very nice performance win for cocotb.

Resolving patch:
diff --git a/cocotb/handle.py b/cocotb/handle.py
--- a/cocotb/handle.py
+++ b/cocotb/handle.py
@@ -31,16 +31,40 @@
 
 import ctypes
 import warnings
+import enum
+from functools import lru_cache
 
 import cocotb
 from cocotb import simulator
-from cocotb.binary import BinaryValue
+from cocotb.binary import BinaryValue, BinaryRepresentation
 from cocotb.log import SimLog
 
 # Only issue a warning for each deprecated attribute access
 _deprecation_warned = set()
 
 
+class _Limits(enum.IntEnum):
+    SIGNED_NBIT   = 1
+    UNSIGNED_NBIT = 2
+    VECTOR_NBIT   = 3
+
+
+@lru_cache(maxsize=None)
+def _value_limits(n_bits, limits):
+    """Calculate min/max for given number of bits and limits class"""
+    if limits == _Limits.SIGNED_NBIT:
+        min_val = -2**(n_bits-1)
+        max_val = 2**(n_bits-1) - 1
+    elif limits == _Limits.UNSIGNED_NBIT:
+        min_val = 0
+        max_val = 2**n_bits - 1
+    else:
+        min_val = -2**(n_bits-1)
+        max_val = 2**n_bits - 1
+
+    return min_val, max_val
+
+
 class SimHandleBase:
     """Base class for all simulation objects.
 
@@ -696,8 +720,10 @@ def _set_value(self, value, call_sim):
                 The value to drive onto the simulator object.
 
         Raises:
-            TypeError: If target is not wide enough or has an unsupported type
-                 for value assignment.
+            TypeError: If target has an unsupported type for value assignment.
+
+            OverflowError: If int value is out of the range that can be represented
+                by the target. -2**(Nbits - 1) <= value <= 2**Nbits - 1
 
         .. deprecated:: 1.5
             :class:`ctypes.Structure` objects are no longer accepted as values for assignment.
@@ -706,17 +732,28 @@ def _set_value(self, value, call_sim):
         """
         value, set_action = self._check_for_set_action(value)
 
-        if isinstance(value, int) and value < 0x7fffffff and len(self) <= 32:
-            call_sim(self, self._handle.set_signal_val_long, set_action, value)
-            return
+        if isinstance(value, int):
+            min_val, max_val = _value_limits(len(self), _Limits.VECTOR_NBIT)
+            if min_val <= value <= max_val:
+                if len(self) <= 32:
+                    call_sim(self, self._handle.set_signal_val_int, set_action, value)
+                    return
+
+                if value < 0:
+                    value = BinaryValue(value=value, n_bits=len(self), bigEndian=False, binaryRepresentation=BinaryRepresentation.TWOS_COMPLEMENT)
+                else:
+                    value = BinaryValue(value=value, n_bits=len(self), bigEndian=False, binaryRepresentation=BinaryRepresentation.UNSIGNED)
+            else:
+                raise OverflowError(
+                    "Int value ({!r}) out of range for assignment of {!r}-bit signal ({!r})"
+                    .format(value, len(self), self._name))
+
         if isinstance(value, ctypes.Structure):
             warnings.warn(
                 "`ctypes.Structure` values are no longer accepted for value assignment. "
                 "Use `BinaryValue(value=bytes(struct_obj), n_bits=len(signal))` instead",
                 DeprecationWarning, stacklevel=3)
             value = BinaryValue(value=cocotb.utils.pack(value), n_bits=len(self))
-        elif isinstance(value, int):
-            value = BinaryValue(value=value, n_bits=len(self), bigEndian=False)
         elif isinstance(value, dict):
             # We're given a dictionary with a list of values and a bit size...
             num = 0
@@ -823,7 +860,13 @@ def _set_value(self, value, call_sim):
                 "Unsupported type for enum value assignment: {} ({!r})"
                 .format(type(value), value))
 
-        call_sim(self, self._handle.set_signal_val_long, set_action, value)
+        min_val, max_val = _value_limits(32, _Limits.UNSIGNED_NBIT)
+        if min_val <= value <= max_val:
+            call_sim(self, self._handle.set_signal_val_int, set_action, value)
+        else:
+            raise OverflowError(
+                "Int value ({!r}) out of range for assignment of enum signal ({!r})"
+                .format(value, self._name))
 
     @ModifiableObject.value.getter
     def value(self) -> int:
@@ -845,6 +888,9 @@ def _set_value(self, value, call_sim):
         Raises:
             TypeError: If target has an unsupported type for
                  integer value assignment.
+
+            OverflowError: If value is out of range for assignment
+                 of 32-bit IntegerObject.
         """
         value, set_action = self._check_for_set_action(value)
 
@@ -855,7 +901,13 @@ def _set_value(self, value, call_sim):
                 "Unsupported type for integer value assignment: {} ({!r})"
                 .format(type(value), value))
 
-        call_sim(self, self._handle.set_signal_val_long, set_action, value)
+        min_val, max_val = _value_limits(32, _Limits.SIGNED_NBIT)
+        if min_val <= value <= max_val:
+            call_sim(self, self._handle.set_signal_val_int, set_action, value)
+        else:
+            raise OverflowError(
+                "Int value ({!r}) out of range for assignment of integer signal ({!r})"
+                .format(value, self._name))
 
     @ModifiableObject.value.getter
     def value(self) -> int:


Test patch:
diff --git a/tests/designs/sample_module/sample_module.sv b/tests/designs/sample_module/sample_module.sv
--- a/tests/designs/sample_module/sample_module.sv
+++ b/tests/designs/sample_module/sample_module.sv
@@ -55,7 +55,10 @@ module sample_module #(
     input  string                               stream_in_string,
 `endif
     input  [7:0]                                stream_in_data,
+    input  [31:0]                               stream_in_data_dword,
+    input  [38:0]                               stream_in_data_39bit,
     input  [63:0]                               stream_in_data_wide,
+    input  [127:0]                              stream_in_data_dqword,
 
     input                                       stream_out_ready,
     output reg [7:0]                            stream_out_data_comb,
diff --git a/tests/designs/sample_module/sample_module.vhdl b/tests/designs/sample_module/sample_module.vhdl
--- a/tests/designs/sample_module/sample_module.vhdl
+++ b/tests/designs/sample_module/sample_module.vhdl
@@ -42,7 +42,10 @@ entity sample_module is
         clk                             : in    std_ulogic;
 
         stream_in_data                  : in    std_ulogic_vector(7 downto 0);
+        stream_in_data_dword            : in    std_ulogic_vector(31 downto 0);
+        stream_in_data_39bit            : in    std_ulogic_vector(38 downto 0);
         stream_in_data_wide             : in    std_ulogic_vector(63 downto 0);
+        stream_in_data_dqword           : in    std_ulogic_vector(127 downto 0);
         stream_in_valid                 : in    std_ulogic;
         stream_in_func_en               : in    std_ulogic;
         stream_in_ready                 : out   std_ulogic;
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
@@ -10,6 +10,7 @@
 from cocotb.triggers import Timer
 
 from common import assert_raises
+from cocotb.handle import _Limits
 
 
 @cocotb.test()
@@ -66,21 +67,198 @@ async def test_delayed_assignment_still_errors(dut):
         dut.stream_in_int <= []
 
 
+async def int_values_test(signal, n_bits, limits=_Limits.VECTOR_NBIT):
+    """Test integer access to a signal."""
+    log = logging.getLogger("cocotb.test")
+    values = gen_int_test_values(n_bits, limits)
+    for val in values:
+        signal <= val
+        await Timer(1, 'ns')
+
+        if limits == _Limits.VECTOR_NBIT:
+            if val < 0:
+                got = signal.value.signed_integer
+            else:
+                got = signal.value.integer
+        else:
+            got = signal.value
+
+        assert got == val, "Expected value {}, got value {}!".format(val, got)
+
+
+def gen_int_test_values(n_bits, limits=_Limits.VECTOR_NBIT):
+    """Generates a list of int test values for a given number of bits."""
+    unsigned_min = 0
+    unsigned_max = 2**n_bits-1
+    signed_min = -2**(n_bits-1)
+    signed_max = 2**(n_bits-1)-1
+
+    if limits == _Limits.VECTOR_NBIT:
+        return [1, -1, 4, -4, unsigned_min, unsigned_max, signed_min, signed_max]
+    elif limits == _Limits.SIGNED_NBIT:
+        return [1, -1, 4, -4, signed_min, signed_max]
+    else:
+        return [1, -1, 4, -4, unsigned_min, unsigned_max]
+
+
+async def int_overflow_test(signal, n_bits, test_mode, limits=_Limits.VECTOR_NBIT):
+    """Test integer overflow."""
+    if test_mode == "ovfl":
+        value = gen_int_ovfl_value(n_bits, limits)
+    elif test_mode == "unfl":
+        value = gen_int_unfl_value(n_bits, limits)
+    else:
+        value = None
+
+    with assert_raises(OverflowError):
+        signal <= value
+
+
+def gen_int_ovfl_value(n_bits, limits=_Limits.VECTOR_NBIT):
+    unsigned_max = 2**n_bits-1
+    signed_max = 2**(n_bits-1)-1
+
+    if limits == _Limits.SIGNED_NBIT:
+        return signed_max + 1
+    elif limits == _Limits.UNSIGNED_NBIT:
+        return unsigned_max + 1
+    else:
+        return unsigned_max + 1
+
+
+def gen_int_unfl_value(n_bits, limits=_Limits.VECTOR_NBIT):
+    unsigned_min = 0
+    signed_min = -2**(n_bits-1)
+
+    if limits == _Limits.SIGNED_NBIT:
+        return signed_min - 1
+    elif limits == _Limits.UNSIGNED_NBIT:
+        return unsigned_min - 1
+    else:
+        return signed_min - 1
+
+
+@cocotb.test()
+async def test_int_8bit(dut):
+    """Test int access to 8-bit vector."""
+    await int_values_test(dut.stream_in_data, len(dut.stream_in_data))
+
+
+@cocotb.test()
+async def test_int_8bit_overflow(dut):
+    """Test 8-bit vector overflow."""
+    await int_overflow_test(dut.stream_in_data, len(dut.stream_in_data), "ovfl")
+
+
+@cocotb.test()
+async def test_int_8bit_underflow(dut):
+    """Test 8-bit vector underflow."""
+    await int_overflow_test(dut.stream_in_data, len(dut.stream_in_data), "unfl")
+
+
+@cocotb.test()
+async def test_int_32bit(dut):
+    """Test int access to 32-bit vector."""
+    await int_values_test(dut.stream_in_data_dword, len(dut.stream_in_data_dword))
+
+
+@cocotb.test()
+async def test_int_32bit_overflow(dut):
+    """Test 32-bit vector overflow."""
+    await int_overflow_test(dut.stream_in_data_dword, len(dut.stream_in_data_dword), "ovfl")
+
+
+@cocotb.test()
+async def test_int_32bit_underflow(dut):
+    """Test 32-bit vector underflow."""
+    await int_overflow_test(dut.stream_in_data_dword, len(dut.stream_in_data_dword), "unfl")
+
+
+@cocotb.test()
+async def test_int_39bit(dut):
+    """Test int access to 39-bit vector."""
+    await int_values_test(dut.stream_in_data_39bit, len(dut.stream_in_data_39bit))
+
+
+@cocotb.test()
+async def test_int_39bit_overflow(dut):
+    """Test 39-bit vector overflow."""
+    await int_overflow_test(dut.stream_in_data_39bit, len(dut.stream_in_data_39bit), "ovfl")
+
+
+@cocotb.test()
+async def test_int_39bit_underflow(dut):
+    """Test 39-bit vector underflow."""
+    await int_overflow_test(dut.stream_in_data_39bit, len(dut.stream_in_data_39bit), "unfl")
+
+
+@cocotb.test()
+async def test_int_64bit(dut):
+    """Test int access to 64-bit vector."""
+    await int_values_test(dut.stream_in_data_wide, len(dut.stream_in_data_wide))
+
+
+@cocotb.test()
+async def test_int_64bit_overflow(dut):
+    """Test 64-bit vector overflow."""
+    await int_overflow_test(dut.stream_in_data_wide, len(dut.stream_in_data_wide), "ovfl")
+
+
+@cocotb.test()
+async def test_int_64bit_underflow(dut):
+    """Test 64-bit vector underflow."""
+    await int_overflow_test(dut.stream_in_data_wide, len(dut.stream_in_data_wide), "unfl")
+
+
+@cocotb.test()
+async def test_int_128bit(dut):
+    """Test int access to 128-bit vector."""
+    await int_values_test(dut.stream_in_data_dqword, len(dut.stream_in_data_dqword))
+
+
+@cocotb.test()
+async def test_int_128bit_overflow(dut):
+    """Test 128-bit vector overflow."""
+    await int_overflow_test(dut.stream_in_data_dqword, len(dut.stream_in_data_dqword), "ovfl")
+
+
+@cocotb.test()
+async def test_int_128bit_underflow(dut):
+    """Test 128-bit vector underflow."""
+    await int_overflow_test(dut.stream_in_data_dqword, len(dut.stream_in_data_dqword), "unfl")
+
+
 @cocotb.test(expect_error=AttributeError if cocotb.SIM_NAME in ["Icarus Verilog"] else ())
 async def test_integer(dut):
-    """
-    Test access to integers
-    """
-    log = logging.getLogger("cocotb.test")
-    await Timer(10, "ns")
-    dut.stream_in_int = 4
-    await Timer(10, "ns")
-    await Timer(10, "ns")
-    got_in = int(dut.stream_out_int)
-    got_out = int(dut.stream_in_int)
-    log.info("dut.stream_out_int = %d" % got_out)
-    log.info("dut.stream_in_int = %d" % got_in)
-    assert got_in == got_out, "stream_in_int and stream_out_int should not match"
+    """Test access to integers."""
+    if cocotb.LANGUAGE in ["verilog"] and cocotb.SIM_NAME.lower().startswith("riviera"):
+        limits = _Limits.VECTOR_NBIT  # stream_in_int is ModifiableObject in riviera, not IntegerObject
+    else:
+        limits = _Limits.SIGNED_NBIT
+
+    await int_values_test(dut.stream_in_int, 32, limits)
+
+
+@cocotb.test(expect_error=AttributeError if cocotb.SIM_NAME in ["Icarus Verilog"] else ())
+async def test_integer_overflow(dut):
+    """Test integer overflow."""
+    if cocotb.LANGUAGE in ["verilog"] and cocotb.SIM_NAME.lower().startswith("riviera"):
+        limits = _Limits.VECTOR_NBIT  # stream_in_int is ModifiableObject in riviera, not IntegerObject
+    else:
+        limits = _Limits.SIGNED_NBIT
+
+    await int_overflow_test(dut.stream_in_int, 32, "ovfl", limits)
+
+
+@cocotb.test(expect_error=AttributeError if cocotb.SIM_NAME in ["Icarus Verilog"] else ())
+async def test_integer_underflow(dut):
+    """Test integer underflow."""
+    if cocotb.LANGUAGE in ["verilog"] and cocotb.SIM_NAME.lower().startswith("riviera"):
+        limits = _Limits.VECTOR_NBIT  # stream_in_int is ModifiableObject in riviera, not IntegerObject
+    else:
+        limits = _Limits.SIGNED_NBIT
+
+    await int_overflow_test(dut.stream_in_int, 32, "unfl", limits)
 
 
 @cocotb.test(expect_error=AttributeError if cocotb.SIM_NAME in ["Icarus Verilog"] else ())
diff --git a/tests/test_cases/test_multi_dimension_array/test_cocotb_array.py b/tests/test_cases/test_multi_dimension_array/test_cocotb_array.py
--- a/tests/test_cases/test_multi_dimension_array/test_cocotb_array.py
+++ b/tests/test_cases/test_multi_dimension_array/test_cocotb_array.py
@@ -117,7 +117,7 @@ async def test_in_vect_packed_packed_packed(dut):
 async def test_in_vect_packed_packed_unpacked(dut):
     await Timer(1, "ns")
     print("Setting: dut.in_vect_packed_packed_unpacked type %s" % type(dut.in_vect_packed_packed_unpacked))
-    dut.in_vect_packed_packed_unpacked = [95869805, 95869805, 95869805]
+    dut.in_vect_packed_packed_unpacked = [365, 365, 365]
     await Timer(1, "ns")
     print("Getting: dut.out_vect_packed_packed_unpacked type %s" % type(dut.out_vect_packed_packed_unpacked))
     if dut.out_vect_packed_packed_unpacked != [365, 365, 365]: