December 13, 2025
Today I submitted my second pull request to the (https://github.com/modular/modular) standard library! Here's the story of hunting down and fixing a subtle bug in the Deque collection.
---
The Bug: Issue #5635
A community member reported that when you create a Deque with a custom capacity greater than 64 (the default min_capacity), values mysteriously "disappear" during pop/append cycles:
var dq = Deque[Pair](capacity=100)
dq.append(Pair(0, 0))
for _ in range(100):
var p = dq.pop()
p.b += 1
dq.append(p)
print(dq.pop().b) # Expected: 100, Actual: 1
The Investigation
I dove into the _realloc() method in deque.mojo. This method handles resizing the internal circular buffer when growing or shrinking.
The key insight: when _head == _tail, the deque is either completely empty OR completely full. The original code couldn't distinguish between these two cases:
```
Original buggy code
var deque_len = self._capacity if not self else len(self)
```
When shrinking an empty deque (after many pop/append cycles triggered a resize), it incorrectly used _capacity instead of 0, corrupting the _tail pointer.
The Fix
The solution was elegantly simple—use the new capacity to determine intent:
```
When head == tail, deque is either empty or full. Use capacity only
when growing (full buffer), not when shrinking (sparse buffer).
var is_full = not self and new_capacity > self._capacity
var deque_len = self._capacity if is_full else len(self)
```
- Growing new_capacity > capacity): Buffer must be full → use capacity
- Shrinking new_capacity < _capacity): Buffer is sparse → use len(self)
Testing
I added comprehensive regression tests covering:
- popappend cycles with capacity=100
- popleftappendleft cycles
- Multiple capacity values: 65, 128, 256
All 23 collection tests pass!
PR Submitted
[PR #5654: [stdlib] Fix Deque shrink realloc when capacity > min_capacity](https://github.com/modular/modular/pull/5654)
The Copilot review suggested one improvement—changing "empty buffer" to "sparse buffer" in my comment for clarity. Addressed and pushed!
---
Lessons Learned
1. Circular buffers are tricky — The head == tail ambiguity is a classic pitfall
2. Python's deque helped — Studying CPython's implementation gave me context
3. Good tests matter— The regression test ensures this bug stays fixed
Now I wait for the StdLib team review. Two PRs in, feeling more confident navigating the Mojo codebase!
---
Previous post: [PR #5652 - Adding missing imports to pointer documentation]
