Skip to content

Commit f06bdae

Browse files
davidperkernestognwAmxx
authored
Add try DoubleEndedQueue functions (#6020)
Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent c2eee49 commit f06bdae

File tree

3 files changed

+156
-28
lines changed

3 files changed

+156
-28
lines changed

.changeset/flat-ideas-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`DoubleEndedQueue`: Add `tryPushBack`, `tryPopBack`, `tryPushFront`, `tryPopFront`, `tryFront`, `tryBack`, and `tryAt` function variants that do not revert.

contracts/utils/structs/DoubleEndedQueue.sol

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,22 @@ library DoubleEndedQueue {
3838
* Reverts with {Panic-RESOURCE_ERROR} if the queue is full.
3939
*/
4040
function pushBack(Bytes32Deque storage deque, bytes32 value) internal {
41+
bool success = tryPushBack(deque, value);
42+
if (!success) Panic.panic(Panic.RESOURCE_ERROR);
43+
}
44+
45+
/**
46+
* @dev Attempts to insert an item at the end of the queue.
47+
*
48+
* Returns `false` if the queue is full. Never reverts.
49+
*/
50+
function tryPushBack(Bytes32Deque storage deque, bytes32 value) internal returns (bool success) {
4151
unchecked {
4252
uint128 backIndex = deque._end;
43-
if (backIndex + 1 == deque._begin) Panic.panic(Panic.RESOURCE_ERROR);
53+
if (backIndex + 1 == deque._begin) return false;
4454
deque._data[backIndex] = value;
4555
deque._end = backIndex + 1;
56+
return true;
4657
}
4758
}
4859

@@ -51,11 +62,23 @@ library DoubleEndedQueue {
5162
*
5263
* Reverts with {Panic-EMPTY_ARRAY_POP} if the queue is empty.
5364
*/
54-
function popBack(Bytes32Deque storage deque) internal returns (bytes32 value) {
65+
function popBack(Bytes32Deque storage deque) internal returns (bytes32) {
66+
(bool success, bytes32 value) = tryPopBack(deque);
67+
if (!success) Panic.panic(Panic.EMPTY_ARRAY_POP);
68+
return value;
69+
}
70+
71+
/**
72+
* @dev Attempts to remove the item at the end of the queue and return it.
73+
*
74+
* Returns `(false, 0x00)` if the queue is empty. Never reverts.
75+
*/
76+
function tryPopBack(Bytes32Deque storage deque) internal returns (bool success, bytes32 value) {
5577
unchecked {
5678
uint128 backIndex = deque._end;
57-
if (backIndex == deque._begin) Panic.panic(Panic.EMPTY_ARRAY_POP);
79+
if (backIndex == deque._begin) return (false, bytes32(0));
5880
--backIndex;
81+
success = true;
5982
value = deque._data[backIndex];
6083
delete deque._data[backIndex];
6184
deque._end = backIndex;
@@ -68,11 +91,22 @@ library DoubleEndedQueue {
6891
* Reverts with {Panic-RESOURCE_ERROR} if the queue is full.
6992
*/
7093
function pushFront(Bytes32Deque storage deque, bytes32 value) internal {
94+
bool success = tryPushFront(deque, value);
95+
if (!success) Panic.panic(Panic.RESOURCE_ERROR);
96+
}
97+
98+
/**
99+
* @dev Attempts to insert an item at the beginning of the queue.
100+
*
101+
* Returns `false` if the queue is full. Never reverts.
102+
*/
103+
function tryPushFront(Bytes32Deque storage deque, bytes32 value) internal returns (bool success) {
71104
unchecked {
72105
uint128 frontIndex = deque._begin - 1;
73-
if (frontIndex == deque._end) Panic.panic(Panic.RESOURCE_ERROR);
106+
if (frontIndex == deque._end) return false;
74107
deque._data[frontIndex] = value;
75108
deque._begin = frontIndex;
109+
return true;
76110
}
77111
}
78112

@@ -81,10 +115,23 @@ library DoubleEndedQueue {
81115
*
82116
* Reverts with {Panic-EMPTY_ARRAY_POP} if the queue is empty.
83117
*/
84-
function popFront(Bytes32Deque storage deque) internal returns (bytes32 value) {
118+
function popFront(Bytes32Deque storage deque) internal returns (bytes32) {
119+
(bool success, bytes32 value) = tryPopFront(deque);
120+
if (!success) Panic.panic(Panic.EMPTY_ARRAY_POP);
121+
return value;
122+
}
123+
124+
/**
125+
* @dev Attempts to remove the item at the beginning of the queue and
126+
* return it.
127+
*
128+
* Returns `(false, 0x00)` if the queue is empty. Never reverts.
129+
*/
130+
function tryPopFront(Bytes32Deque storage deque) internal returns (bool success, bytes32 value) {
85131
unchecked {
86132
uint128 frontIndex = deque._begin;
87-
if (frontIndex == deque._end) Panic.panic(Panic.EMPTY_ARRAY_POP);
133+
if (frontIndex == deque._end) return (false, bytes32(0));
134+
success = true;
88135
value = deque._data[frontIndex];
89136
delete deque._data[frontIndex];
90137
deque._begin = frontIndex + 1;
@@ -96,20 +143,42 @@ library DoubleEndedQueue {
96143
*
97144
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if the queue is empty.
98145
*/
99-
function front(Bytes32Deque storage deque) internal view returns (bytes32 value) {
100-
if (empty(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
101-
return deque._data[deque._begin];
146+
function front(Bytes32Deque storage deque) internal view returns (bytes32) {
147+
(bool success, bytes32 value) = tryFront(deque);
148+
if (!success) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
149+
return value;
150+
}
151+
152+
/**
153+
* @dev Attempts to return the item at the beginning of the queue.
154+
*
155+
* Returns `(false, 0x00)` if the queue is empty. Never reverts.
156+
*/
157+
function tryFront(Bytes32Deque storage deque) internal view returns (bool success, bytes32 value) {
158+
if (empty(deque)) return (false, bytes32(0));
159+
return (true, deque._data[deque._begin]);
102160
}
103161

104162
/**
105163
* @dev Returns the item at the end of the queue.
106164
*
107165
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if the queue is empty.
108166
*/
109-
function back(Bytes32Deque storage deque) internal view returns (bytes32 value) {
110-
if (empty(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
167+
function back(Bytes32Deque storage deque) internal view returns (bytes32) {
168+
(bool success, bytes32 value) = tryBack(deque);
169+
if (!success) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
170+
return value;
171+
}
172+
173+
/**
174+
* @dev Attempts to return the item at the end of the queue.
175+
*
176+
* Returns `(false, 0x00)` if the queue is empty. Never reverts.
177+
*/
178+
function tryBack(Bytes32Deque storage deque) internal view returns (bool success, bytes32 value) {
179+
if (empty(deque)) return (false, bytes32(0));
111180
unchecked {
112-
return deque._data[deque._end - 1];
181+
return (true, deque._data[deque._end - 1]);
113182
}
114183
}
115184

@@ -119,11 +188,23 @@ library DoubleEndedQueue {
119188
*
120189
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if the index is out of bounds.
121190
*/
122-
function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32 value) {
123-
if (index >= length(deque)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
191+
function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32) {
192+
(bool success, bytes32 value) = tryAt(deque, index);
193+
if (!success) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
194+
return value;
195+
}
196+
197+
/**
198+
* @dev Attempts to return the item at a position in the queue given by `index`, with the first item at
199+
* 0 and the last item at `length(deque) - 1`.
200+
*
201+
* Returns `(false, 0x00)` if the index is out of bounds. Never reverts.
202+
*/
203+
function tryAt(Bytes32Deque storage deque, uint256 index) internal view returns (bool success, bytes32 value) {
204+
if (index >= length(deque)) return (false, bytes32(0));
124205
// By construction, length is a uint128, so the check above ensures that index can be safely downcast to uint128
125206
unchecked {
126-
return deque._data[deque._begin + uint128(index)];
207+
return (true, deque._data[deque._begin + uint128(index)]);
127208
}
128209
}
129210

test/utils/structs/DoubleEndedQueue.test.js

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ describe('DoubleEndedQueue', function () {
2626

2727
describe('when empty', function () {
2828
it('getters', async function () {
29-
expect(await this.mock.$empty(0)).to.be.true;
30-
expect(await this.getContent()).to.have.ordered.members([]);
29+
await expect(this.mock.$empty(0)).to.eventually.be.true;
30+
await expect(this.getContent()).to.eventually.have.ordered.members([]);
3131
});
3232

3333
it('reverts on accesses', async function () {
@@ -36,6 +36,24 @@ describe('DoubleEndedQueue', function () {
3636
await expect(this.mock.$back(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
3737
await expect(this.mock.$front(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
3838
});
39+
40+
it('try getters return false/zero on empty', async function () {
41+
await expect(this.mock.$tryFront(0)).to.eventually.deep.equal([false, ethers.ZeroHash]);
42+
await expect(this.mock.$tryBack(0)).to.eventually.deep.equal([false, ethers.ZeroHash]);
43+
await expect(this.mock.$tryAt(0, 0)).to.eventually.deep.equal([false, ethers.ZeroHash]);
44+
});
45+
46+
it('try pops return false/zero on empty', async function () {
47+
await expect(this.mock.$tryPopFront(0)).to.emit(this.mock, 'return$tryPopFront').withArgs(false, ethers.ZeroHash);
48+
await expect(this.mock.$tryPopBack(0)).to.emit(this.mock, 'return$tryPopBack').withArgs(false, ethers.ZeroHash);
49+
});
50+
51+
it('try pushes succeed on empty', async function () {
52+
await expect(this.mock.$tryPushFront(0, bytesA)).to.emit(this.mock, 'return$tryPushFront').withArgs(true);
53+
await expect(this.mock.$tryPushBack(0, bytesB)).to.emit(this.mock, 'return$tryPushBack').withArgs(true);
54+
55+
await expect(this.getContent()).to.eventually.have.ordered.members([bytesA, bytesB]);
56+
});
3957
});
4058

4159
describe('when not empty', function () {
@@ -47,11 +65,11 @@ describe('DoubleEndedQueue', function () {
4765
});
4866

4967
it('getters', async function () {
50-
expect(await this.mock.$empty(0)).to.be.false;
51-
expect(await this.mock.$length(0)).to.equal(this.content.length);
52-
expect(await this.mock.$front(0)).to.equal(this.content[0]);
53-
expect(await this.mock.$back(0)).to.equal(this.content[this.content.length - 1]);
54-
expect(await this.getContent()).to.have.ordered.members(this.content);
68+
await expect(this.mock.$empty(0)).to.eventually.be.false;
69+
await expect(this.mock.$length(0)).to.eventually.equal(this.content.length);
70+
await expect(this.mock.$front(0)).to.eventually.equal(this.content[0]);
71+
await expect(this.mock.$back(0)).to.eventually.equal(this.content[this.content.length - 1]);
72+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
5573
});
5674

5775
it('out of bounds access', async function () {
@@ -60,19 +78,29 @@ describe('DoubleEndedQueue', function () {
6078
);
6179
});
6280

81+
it('try getters return true/value when not empty', async function () {
82+
await expect(this.mock.$tryFront(0)).to.eventually.deep.equal([true, this.content[0]]);
83+
await expect(this.mock.$tryBack(0)).to.eventually.deep.equal([true, this.content[this.content.length - 1]]);
84+
await expect(this.mock.$tryAt(0, 1)).to.eventually.deep.equal([true, this.content[1]]);
85+
});
86+
87+
it('tryAt returns false/zero on out of bounds', async function () {
88+
await expect(this.mock.$tryAt(0, this.content.length)).to.eventually.deep.equal([false, ethers.ZeroHash]);
89+
});
90+
6391
describe('push', function () {
6492
it('front', async function () {
6593
await this.mock.$pushFront(0, bytesD);
6694
this.content.unshift(bytesD); // add element at the beginning
6795

68-
expect(await this.getContent()).to.have.ordered.members(this.content);
96+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
6997
});
7098

7199
it('back', async function () {
72100
await this.mock.$pushBack(0, bytesD);
73101
this.content.push(bytesD); // add element at the end
74102

75-
expect(await this.getContent()).to.have.ordered.members(this.content);
103+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
76104
});
77105
});
78106

@@ -81,22 +109,36 @@ describe('DoubleEndedQueue', function () {
81109
const value = this.content.shift(); // remove first element
82110
await expect(this.mock.$popFront(0)).to.emit(this.mock, 'return$popFront').withArgs(value);
83111

84-
expect(await this.getContent()).to.have.ordered.members(this.content);
112+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
85113
});
86114

87115
it('back', async function () {
88116
const value = this.content.pop(); // remove last element
89117
await expect(this.mock.$popBack(0)).to.emit(this.mock, 'return$popBack').withArgs(value);
90118

91-
expect(await this.getContent()).to.have.ordered.members(this.content);
119+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
120+
});
121+
122+
it('try front', async function () {
123+
const value = this.content.shift();
124+
await expect(this.mock.$tryPopFront(0)).to.emit(this.mock, 'return$tryPopFront').withArgs(true, value);
125+
126+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
127+
});
128+
129+
it('try back', async function () {
130+
const value = this.content.pop();
131+
await expect(this.mock.$tryPopBack(0)).to.emit(this.mock, 'return$tryPopBack').withArgs(true, value);
132+
133+
await expect(this.getContent()).to.eventually.have.ordered.members(this.content);
92134
});
93135
});
94136

95137
it('clear', async function () {
96138
await this.mock.$clear(0);
97139

98-
expect(await this.mock.$empty(0)).to.be.true;
99-
expect(await this.getContent()).to.have.ordered.members([]);
140+
await expect(this.mock.$empty(0)).to.eventually.be.true;
141+
await expect(this.getContent()).to.eventually.have.ordered.members([]);
100142
});
101143
});
102144
});

0 commit comments

Comments
 (0)