Skip to content

feat: memory fixed size stack #5780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rude-eggs-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

Added FixedSizeMemoryStack a gas‑efficient, in‑memory, fixed‑capacity stack
98 changes: 98 additions & 0 deletions contracts/utils/FixedSizeMemoryStack.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title FixedSizeMemoryStack
/// @notice A gas‑efficient, in‑memory, fixed‑capacity stack (first in, last out) for arbitrary 32‑byte values.
/// @dev
/// * Designed for algorithmic helpers (sorting, DFS/BFS, expression evaluation, etc.).
/// * **Not** suitable for persisting data between external calls – memory is transient.
/// * Functions are `internal`+`pure` so that the optimizer can aggressively inline them.
/// *
/// * The structure is designed to perform the following operations with the corresponding complexities:
/// * * push (insert a value onto the stack): O(1)
/// * * pop (remove the top value from the stack): O(1)
/// * * peek (return the top value from the stack): O(1)
/// * * size (return the number of elements in the stack): O(1)
/// * * capacity (return the maximum number of elements the stack can hold): O(1)
/// * * isEmpty (return whether the stack is empty): O(1)
/// * * isFull (return whether the stack is at full capacity): O(1)
/// * * clear (remove all elements from the stack): O(1)
library FixedSizeMemoryStack {
/// @notice Pushed more items than the stack’s capacity.
/// @param capacity Maximum number of elements the stack can hold.
error StackOverflow(uint256 capacity);

/// @notice Popped or peeked when the stack was empty.
error StackUnderflow();

/// @notice Attempted to create a stack with zero capacity.
error ZeroCapacity();

struct Stack {
bytes32[] _data; // pre‑allocated fixed‑length array
uint256 _top; // next insertion index
}

/// @notice Initialise a new fixed‑size stack in memory.
/// @param maxSize The maximum number of elements the stack can hold (> 0).
/// @return stack A fully initialised, empty stack.
function init(uint256 maxSize) internal pure returns (Stack memory stack) {
if (maxSize == 0) revert ZeroCapacity();
stack._data = new bytes32[](maxSize);
stack._top = 0;
}

/// @notice Push a value onto the stack.
/// @param stack The stack to mutate (memory reference).
/// @param value The value to push.
function push(Stack memory stack, bytes32 value) internal pure {
uint256 t = stack._top;
if (t >= stack._data.length) revert StackOverflow(stack._data.length);
stack._data[t] = value;
stack._top = t + 1;
}

/// @notice Pop the top value from the stack.
/// @param stack The stack to mutate (memory reference).
/// @return value The element removed from the stack.
function pop(Stack memory stack) internal pure returns (bytes32 value) {
uint256 t = stack._top;
if (t == 0) revert StackUnderflow();
t -= 1;
value = stack._data[t];
stack._top = t;
}

/// @notice Return, but do **not** remove, the element on top of the stack.
function peek(Stack memory stack) internal pure returns (bytes32 value) {
uint256 t = stack._top;
if (t == 0) revert StackUnderflow();
value = stack._data[t - 1];
}

/// @notice Current number of stored elements.
function size(Stack memory stack) internal pure returns (uint256) {
return stack._top;
}

/// @notice Maximum number of elements the stack can hold.
function capacity(Stack memory stack) internal pure returns (uint256) {
return stack._data.length;
}

/// @notice Whether the stack currently holds zero elements.
function isEmpty(Stack memory stack) internal pure returns (bool) {
return stack._top == 0;
}

/// @notice Whether the stack is at full capacity.
function isFull(Stack memory stack) internal pure returns (bool) {
return stack._top == stack._data.length;
}

/// @notice Reset the stack to empty **without** reallocating memory.
/// @dev Sets the logical size to zero; underlying array remains allocated.
function clear(Stack memory stack) internal pure {
stack._top = 0;
}
}
124 changes: 124 additions & 0 deletions test/utils/FixedSizeMemoryStack.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {FixedSizeMemoryStack} from "../../contracts/utils/FixedSizeMemoryStack.sol";

contract FixedSizeMemoryStackTest is Test, SymTest {
using FixedSizeMemoryStack for FixedSizeMemoryStack.Stack;

function testFuzzPush(bytes32 item) public pure {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);
stack.push(item);

assertEq(stack._top, 1);
assertEq(stack._data[0], item);
}

/// forge-config: default.allow_internal_expect_revert = true
function testPop() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

vm.expectRevert((FixedSizeMemoryStack.StackUnderflow.selector));
stack.pop();

bytes32 item1 = svm.createBytes32("item1");
bytes32 item2 = svm.createBytes32("item2");

stack.push(item1);
stack.push(item2);

assertEq(stack.pop(), item2);
assertEq(stack._top, 1);
assertEq(stack.pop(), item1);
assertEq(stack._top, 0);
}

/// forge-config: default.allow_internal_expect_revert = true
function testPeekStackUnderflow() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

vm.expectRevert(abi.encodeWithSelector(FixedSizeMemoryStack.StackUnderflow.selector));
stack.peek();
assertEq(stack._top, 0);
}

function testPeek() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

stack.push(bytes32(uint256(1)));
stack.push(bytes32(uint256(2)));

assertEq(stack.peek(), bytes32(uint256(2)));
assertEq(stack._top, 2);
}

function testSize() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

assertEq(stack.size(), 0);

stack.push(bytes32(uint256(1)));
assertEq(stack.size(), 1);

stack.push(bytes32(uint256(2)));
assertEq(stack.size(), 2);

stack.pop();
assertEq(stack.size(), 1);

stack.pop();
assertEq(stack.size(), 0);
}

function testCapacity() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

assertEq(stack.capacity(), 10);
}

function testIsEmpty() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

assertEq(stack.isEmpty(), true);

stack.push(bytes32(uint256(1)));
assertEq(stack.isEmpty(), false);

stack.pop();
assertEq(stack.isEmpty(), true);
}

function testIsFull() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

assertEq(stack.isFull(), false);

stack.push(bytes32(uint256(1)));
assertEq(stack.isFull(), false);

for (uint256 i = 0; i < 9; i++) {
stack.push(bytes32(uint256(i)));
}

assertEq(stack.isFull(), true);

stack.pop();
assertEq(stack.isFull(), false);
}

function testClear() public {
FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10);

stack.push(bytes32(uint256(1)));
stack.push(bytes32(uint256(2)));

stack.clear();

assertEq(stack.size(), 0);
assertEq(stack.isEmpty(), true);
assertEq(stack.isFull(), false);
assertEq(stack._top, 0);
}
}