Skip to content

Commit 6614ddd

Browse files
committed
feat: add utility library FixedSizeMemoryStack
1 parent 6e14ecc commit 6614ddd

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// SPDX‑License‑Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
/// @title FixedSizeMemoryStack
5+
/// @notice A gas‑efficient, in‑memory, fixed‑capacity stack (first in, last out) for arbitrary 32‑byte values.
6+
/// @dev
7+
/// * Designed for algorithmic helpers (sorting, DFS/BFS, expression evaluation, etc.).
8+
/// * **Not** suitable for persisting data between external calls – memory is transient.
9+
/// * Functions are `internal`+`pure` so that the optimizer can aggressively inline them.
10+
/// *
11+
/// * The structure is designed to perform the following operations with the corresponding complexities:
12+
/// * * push (insert a value onto the stack): O(1)
13+
/// * * pop (remove the top value from the stack): O(1)
14+
/// * * peek (return the top value from the stack): O(1)
15+
/// * * size (return the number of elements in the stack): O(1)
16+
/// * * capacity (return the maximum number of elements the stack can hold): O(1)
17+
/// * * isEmpty (return whether the stack is empty): O(1)
18+
/// * * isFull (return whether the stack is at full capacity): O(1)
19+
/// * * clear (remove all elements from the stack): O(1)
20+
library FixedSizeMemoryStack {
21+
/// @notice Pushed more items than the stack’s capacity.
22+
/// @param capacity Maximum number of elements the stack can hold.
23+
error StackOverflow(uint256 capacity);
24+
25+
/// @notice Popped or peeked when the stack was empty.
26+
error StackUnderflow();
27+
28+
/// @notice Attempted to create a stack with zero capacity.
29+
error ZeroCapacity();
30+
31+
struct Stack {
32+
bytes32[] _data; // pre‑allocated fixed‑length array
33+
uint256 _top; // next insertion index
34+
}
35+
36+
/// @notice Initialise a new fixed‑size stack in memory.
37+
/// @param maxSize The maximum number of elements the stack can hold (> 0).
38+
/// @return stack A fully initialised, empty stack.
39+
function init(uint256 maxSize) internal pure returns (Stack memory stack) {
40+
if (maxSize == 0) revert ZeroCapacity();
41+
stack._data = new bytes32[](maxSize);
42+
stack._top = 0;
43+
}
44+
45+
/// @notice Push a value onto the stack.
46+
/// @param stack The stack to mutate (memory reference).
47+
/// @param value The value to push.
48+
function push(Stack memory stack, bytes32 value) internal pure {
49+
uint256 t = stack._top;
50+
if (t >= stack._data.length) revert StackOverflow(stack._data.length);
51+
stack._data[t] = value;
52+
unchecked {
53+
stack._top = t + 1;
54+
}
55+
}
56+
57+
/// @notice Pop the top value from the stack.
58+
/// @param stack The stack to mutate (memory reference).
59+
/// @return value The element removed from the stack.
60+
function pop(Stack memory stack) internal pure returns (bytes32 value) {
61+
uint256 t = stack._top;
62+
if (t == 0) revert StackUnderflow();
63+
unchecked {
64+
t -= 1;
65+
}
66+
value = stack._data[t];
67+
stack._top = t;
68+
}
69+
70+
/// @notice Return, but do **not** remove, the element on top of the stack.
71+
function peek(Stack memory stack) internal pure returns (bytes32 value) {
72+
uint256 t = stack._top;
73+
if (t == 0) revert StackUnderflow();
74+
value = stack._data[t - 1];
75+
}
76+
77+
/// @notice Current number of stored elements.
78+
function size(Stack memory stack) internal pure returns (uint256) {
79+
return stack._top;
80+
}
81+
82+
/// @notice Maximum number of elements the stack can hold.
83+
function capacity(Stack memory stack) internal pure returns (uint256) {
84+
return stack._data.length;
85+
}
86+
87+
/// @notice Whether the stack currently holds zero elements.
88+
function isEmpty(Stack memory stack) internal pure returns (bool) {
89+
return stack._top == 0;
90+
}
91+
92+
/// @notice Whether the stack is at full capacity.
93+
function isFull(Stack memory stack) internal pure returns (bool) {
94+
return stack._top == stack._data.length;
95+
}
96+
97+
/// @notice Reset the stack to empty **without** reallocating memory.
98+
/// @dev Sets the logical size to zero; underlying array remains allocated.
99+
function clear(Stack memory stack) internal pure {
100+
stack._top = 0;
101+
}
102+
}

test/utils/FixedSizeMemoryStack.t.sol

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Test} from "forge-std/Test.sol";
6+
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
7+
import {FixedSizeMemoryStack} from "../../contracts/utils/FixedSizeMemoryStack.sol";
8+
9+
contract FixedSizeMemoryStackTest is Test, SymTest {
10+
using FixedSizeMemoryStack for FixedSizeMemoryStack.Stack;
11+
12+
function testFuzzPush(bytes32 item) public pure {
13+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
14+
stack.push(item);
15+
16+
assertEq(stack._top, 1);
17+
assertEq(stack._data[0], item);
18+
}
19+
20+
/// forge-config: default.allow_internal_expect_revert = true
21+
function testPop() public {
22+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
23+
24+
vm.expectRevert((FixedSizeMemoryStack.StackUnderflow.selector));
25+
stack.pop();
26+
27+
bytes32 item1 = svm.createBytes32("item1");
28+
bytes32 item2 = svm.createBytes32("item2");
29+
30+
stack.push(item1);
31+
stack.push(item2);
32+
33+
assertEq(stack.pop(), item2);
34+
assertEq(stack._top, 1);
35+
assertEq(stack.pop(), item1);
36+
assertEq(stack._top, 0);
37+
}
38+
39+
/// forge-config: default.allow_internal_expect_revert = true
40+
function testPeek() public {
41+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
42+
43+
vm.expectRevert(abi.encodeWithSelector(FixedSizeMemoryStack.StackUnderflow.selector));
44+
stack.peek();
45+
assertEq(stack._top, 0);
46+
47+
stack.push(bytes32(uint256(1)));
48+
stack.push(bytes32(uint256(2)));
49+
50+
assertEq(stack.peek(), bytes32(uint256(2)));
51+
assertEq(stack._top, 2);
52+
}
53+
54+
function testSize() public {
55+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
56+
57+
assertEq(stack.size(), 0);
58+
59+
stack.push(bytes32(uint256(1)));
60+
assertEq(stack.size(), 1);
61+
62+
stack.push(bytes32(uint256(2)));
63+
assertEq(stack.size(), 2);
64+
65+
stack.pop();
66+
assertEq(stack.size(), 1);
67+
68+
stack.pop();
69+
assertEq(stack.size(), 0);
70+
}
71+
72+
function testCapacity() public {
73+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
74+
75+
assertEq(stack.capacity(), 10);
76+
}
77+
78+
function testIsEmpty() public {
79+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
80+
81+
assertEq(stack.isEmpty(), true);
82+
83+
stack.push(bytes32(uint256(1)));
84+
assertEq(stack.isEmpty(), false);
85+
86+
stack.pop();
87+
assertEq(stack.isEmpty(), true);
88+
}
89+
90+
function testIsFull() public {
91+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
92+
93+
assertEq(stack.isFull(), false);
94+
95+
stack.push(bytes32(uint256(1)));
96+
assertEq(stack.isFull(), false);
97+
98+
for (uint256 i = 0; i < 9; i++) {
99+
stack.push(bytes32(uint256(i)));
100+
}
101+
102+
assertEq(stack.isFull(), true);
103+
104+
stack.pop();
105+
assertEq(stack.isFull(), false);
106+
}
107+
108+
function testClear() public {
109+
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
110+
111+
stack.push(bytes32(uint256(1)));
112+
stack.push(bytes32(uint256(2)));
113+
114+
stack.clear();
115+
116+
assertEq(stack.size(), 0);
117+
assertEq(stack.isEmpty(), true);
118+
assertEq(stack.isFull(), false);
119+
assertEq(stack._top, 0);
120+
}
121+
}

0 commit comments

Comments
 (0)