Repository: cocotb/cocotb

Issue:
Handle writes not called in the order they were scheduled
Based on gitter discussion here:
https://gitter.im/cocotb/Lobby?at=61a061ee8853d316401680f3
Consider the example:
```
a.b.array= [0xFFFF,0xFFFF,0xFFFF]
a.b.array[0][0]=0
await Timer(1)
print ("ROW - %d=%x"% (0,a.b.array[0].value) )
```

this should print` ROW - 0=0x7FFF` but `prints 0xFFFF`.

This appears to be because of a bug in how the following data structure is being used in scheduler.py

`self._write_calls = _py_compat.insertion_ordered_dict()`

The handle for `a.b.array[0][0]=0` is being executed before the handle for `a.b.array[0]=0xFFFF`

@ktbarrett shared a fix which appears to work for me here:  https://gist.github.com/ktbarrett/874008b482f7f3ff618cacd4836f761b


Discussion:

Resolving patch:
diff --git a/cocotb/scheduler.py b/cocotb/scheduler.py
--- a/cocotb/scheduler.py
+++ b/cocotb/scheduler.py
@@ -40,6 +40,7 @@
 import inspect
 import warnings
 from typing import Any, Union
+from collections import OrderedDict
 from collections.abc import Coroutine
 
 import cocotb
@@ -231,9 +232,10 @@ def __init__(self):
         # Our main state
         self._mode = Scheduler._MODE_NORMAL
 
-        # A dictionary of pending (write_func, args), keyed by handle. Only the last scheduled write
-        # in a timestep is performed, all the rest are discarded in python.
-        self._write_calls = _py_compat.insertion_ordered_dict()
+        # A dictionary of pending (write_func, args), keyed by handle.
+        # Writes are applied oldest to newest (least recently used).
+        # Only the last scheduled write to a particular handle in a timestep is performed.
+        self._write_calls = OrderedDict()
 
         self._pending_coros = []
         self._pending_triggers = []
@@ -261,7 +263,7 @@ async def _do_writes(self):
             await self._read_write
 
             while self._write_calls:
-                handle, (func, args) = self._write_calls.popitem()
+                handle, (func, args) = self._write_calls.popitem(last=False)
                 func(*args)
             self._writes_pending.clear()
 
@@ -286,7 +288,7 @@ def _check_termination(self):
             self._timer1.prime(self._test_completed)
             self._trigger2coros = _py_compat.insertion_ordered_dict()
             self._terminate = False
-            self._write_calls = _py_compat.insertion_ordered_dict()
+            self._write_calls = OrderedDict()
             self._writes_pending.clear()
             self._mode = Scheduler._MODE_TERM
 
@@ -555,6 +557,8 @@ def _schedule_write(self, handle, write_func, *args):
         if self._write_coro_inst is None:
             self._write_coro_inst = self.add(self._do_writes())
 
+        if handle in self._write_calls:
+            del self._write_calls[handle]
         self._write_calls[handle] = (write_func, args)
         self._writes_pending.set()
 


Test patch:
diff --git a/tests/test_cases/test_cocotb/test_scheduler.py b/tests/test_cases/test_cocotb/test_scheduler.py
--- a/tests/test_cases/test_cocotb/test_scheduler.py
+++ b/tests/test_cases/test_cocotb/test_scheduler.py
@@ -331,6 +331,23 @@ async def test_last_scheduled_write_wins_array(dut):
     assert dut.array_7_downto_4.value == [10, 2, 3, 4]
 
 
+# Most simulators do not support setting the value of a single bit of a packed array
+@cocotb.test(
+    skip=not cocotb.SIM_NAME.lower().startswith(("modelsim", "riviera"))
+    or cocotb.LANGUAGE != "vhdl"
+)
+async def test_last_scheduled_write_wins_array_handle_alias(dut):
+    """
+    Tests case where handles in the scheduled writes cache in the scheduler alias
+    """
+    dut.array_7_downto_4.value = [0, 0, 0, 0]
+    dut.array_7_downto_4[7][0].value = 1
+
+    await ReadOnly()
+
+    assert dut.array_7_downto_4.value == [1, 0, 0, 0]
+
+
 @cocotb.test()
 async def test_task_repr(dut):
     """Test RunningTask.__repr__."""