Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4fec530
implement binary heap
Amxx Jun 16, 2024
8fa2eeb
codespell & lib naming
Amxx Jun 16, 2024
792fcba
tests
Amxx Jun 16, 2024
0c86005
fix fuzzing tests
Amxx Jun 16, 2024
248baf6
codespell
Amxx Jun 16, 2024
53db2ab
update
Amxx Jun 17, 2024
945e0f4
procedural generation
Amxx Jun 17, 2024
df82b15
testing
Amxx Jun 17, 2024
8b965fc
overflow handling
Amxx Jun 21, 2024
e952cf6
add replace and changeset
Amxx Jun 21, 2024
f5fa274
rename top -> peek
Amxx Jun 21, 2024
1f0fef0
internal renaming
Amxx Jun 21, 2024
d0972a3
codespell
Amxx Jun 21, 2024
8e3dda6
regenerate
Amxx Jun 21, 2024
38e1813
auto regenerate
Amxx Jun 21, 2024
02f224d
Update .githooks/pre-push
Amxx Jun 21, 2024
7e88481
up
Amxx Jun 21, 2024
a46cc63
Merge branch 'master' into struct/heap
Amxx Jun 21, 2024
b2fda31
up
Amxx Jun 21, 2024
516f1ca
tests
Amxx Jun 21, 2024
cf1278e
Update test/utils/structs/Heap.test.js
Amxx Jun 21, 2024
5f15d1c
Update test/utils/structs/Heap.test.js
Amxx Jun 21, 2024
32e9b49
Apply suggestions from code review
Amxx Jun 27, 2024
c083d79
regenrate
Amxx Jun 27, 2024
0e6ada0
Merge branch 'master' into struct/heap
Amxx Jul 3, 2024
7c98102
update inline comments
Amxx Jul 15, 2024
a1767d4
update
Amxx Jul 15, 2024
1c1e84b
Address comment for the PR
Amxx Jul 16, 2024
0e7fe7a
rewrite Arrays.sol to use uint256[] as the default, and use Comparato…
Amxx Jul 17, 2024
d495859
Update scripts/generate/templates/Heap.js
Amxx Jul 18, 2024
fe8e902
regenerate
Amxx Jul 18, 2024
3abeb84
Add docs
ernestognw Jul 23, 2024
8801d98
Update scripts/generate/templates/Heap.js
Amxx Jul 23, 2024
f78df0c
Apply suggestions from code review
Amxx Jul 23, 2024
bb37dfb
fix generation + change key type
Amxx Jul 23, 2024
1fb4b81
more invariant check
Amxx Jul 23, 2024
d3308c4
Update scripts/generate/templates/Heap.js
ernestognw Jul 23, 2024
5b07512
Generate
ernestognw Jul 23, 2024
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
Prev Previous commit
Next Next commit
tests
  • Loading branch information
Amxx committed Jun 16, 2024
commit 792fcbae2dc5eb4a164cd5b84fae6116ba66b610
6 changes: 6 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures.
* {CircularBuffer}: A data structure to store the last N values pushed to it.
* {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time.
* {Heap}: A library that implement https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage.
* {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions.
* {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly.
* {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type.
Expand All @@ -36,6 +37,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Context}: An utility for abstracting the sender and calldata in the current execution context.
* {Packing}: A library for packing and unpacking multiple values into bytes32
* {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes].
* {Comparators}: A library that contains comparator functions to use with with the {Heap} library.

[NOTE]
====
Expand Down Expand Up @@ -100,6 +102,8 @@ Ethereum contracts have no native concept of an interface, so applications must

{{Checkpoints}}

{{Heap}}

{{MerkleTree}}

== Libraries
Expand Down Expand Up @@ -127,3 +131,5 @@ Ethereum contracts have no native concept of an interface, so applications must
{{Packing}}

{{Panic}}

{{Comparators}}
36 changes: 22 additions & 14 deletions contracts/utils/structs/Heap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ library Heap {
Uint256Heap storage self,
function(uint256, uint256) view returns (bool) comp
) internal returns (uint256) {
uint32 length = size(self);
uint32 size = length(self);

if (length == 0) Panic.panic(Panic.EMPTY_ARRAY_POP);
if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP);

uint32 last = length - 1; // could be unchecked (check above)
uint32 last = size - 1; // could be unchecked (check above)

// get root location (in the data array) and value
uint32 rootIdx = self.data[0].index;
Expand Down Expand Up @@ -128,18 +128,26 @@ library Heap {
uint256 value,
function(uint256, uint256) view returns (bool) comp
) internal {
uint32 length = size(self);
self.data.push(Uint256HeapNode({index: length, lookup: length, value: value}));
_heapifyUp(self, length, value, comp);
uint32 size = length(self);
self.data.push(Uint256HeapNode({index: size, lookup: size, value: value}));
_heapifyUp(self, size, value, comp);
}

/**
* @dev Returns the number of elements in the heap.
*/
function size(Uint256Heap storage self) internal view returns (uint32) {
function length(Uint256Heap storage self) internal view returns (uint32) {
return self.data.length.toUint32();
}

function clear(Uint256Heap storage self) internal {
Uint256HeapNode[] storage data = self.data;
/// @solidity memory-safe-assembly
assembly {
sstore(data.slot, 0)
}
}

/*
* @dev Swap node `i` and `j` in the tree.
*/
Expand All @@ -158,37 +166,37 @@ library Heap {
* @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a
* comparator, and moving toward the leafs of the underlying tree.
*
* Note: This is a private function that is called in a trusted context with already cached parameters. `length`
* Note: This is a private function that is called in a trusted context with already cached parameters. `lesizength`
* and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These
* parameters are not verified. It is the caller role to make sure the parameters are correct.
*/
function _heapifyDown(
Uint256Heap storage self,
uint32 length,
uint32 size,
uint32 pos,
uint256 value,
function(uint256, uint256) view returns (bool) comp
) private {
uint32 left = 2 * pos + 1;
uint32 right = 2 * pos + 2;

if (right < length) {
if (right < size) {
uint256 lValue = self.data[self.data[left].index].value;
uint256 rValue = self.data[self.data[right].index].value;
if (comp(lValue, value) || comp(rValue, value)) {
if (comp(lValue, rValue)) {
_swap(self, pos, left);
_heapifyDown(self, length, left, value, comp);
_heapifyDown(self, size, left, value, comp);
} else {
_swap(self, pos, right);
_heapifyDown(self, length, right, value, comp);
_heapifyDown(self, size, right, value, comp);
}
}
} else if (left < length) {
} else if (left < size) {
uint256 lValue = self.data[self.data[left].index].value;
if (comp(lValue, value)) {
_swap(self, pos, left);
_heapifyDown(self, length, left, value, comp);
_heapifyDown(self, size, left, value, comp);
}
}
}
Expand Down
70 changes: 7 additions & 63 deletions test/utils/structs/Heap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contract HeapTest is Test {
Heap.Uint256Heap internal heap;

function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal {
for (uint32 i = 0; i < heap.size(); ++i) {
for (uint32 i = 0; i < heap.length(); ++i) {
// lookups
assertEq(i, heap.data[heap.data[i].index].lookup);

Expand All @@ -23,74 +23,14 @@ contract HeapTest is Test {
}
}

function testUnit() public {
// <empty>
assertEq(heap.size(), 0);
_validateHeap(Comparators.lt);

heap.insert(712); // 712
assertEq(heap.size(), 1);
_validateHeap(Comparators.lt);

heap.insert(20); // 20, 712
assertEq(heap.size(), 2);
_validateHeap(Comparators.lt);

heap.insert(4337); // 20, 712, 4337
assertEq(heap.size(), 3);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 20); // 712, 4337
assertEq(heap.size(), 2);
_validateHeap(Comparators.lt);

heap.insert(1559); // 712, 1559, 4337
assertEq(heap.size(), 3);
_validateHeap(Comparators.lt);

heap.insert(155); // 155, 712, 1559, 4337
assertEq(heap.size(), 4);
_validateHeap(Comparators.lt);

heap.insert(7702); // 155, 712, 1559, 4337, 7702
assertEq(heap.size(), 5);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 155); // 712, 1559, 4337, 7702
assertEq(heap.size(), 4);
_validateHeap(Comparators.lt);

heap.insert(721); // 712, 721, 1559, 4337, 7702
assertEq(heap.size(), 5);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 712); // 721, 1559, 4337, 7702
assertEq(heap.size(), 4);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 721); // 1559, 4337, 7702
assertEq(heap.size(), 3);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 1559); // 4337, 7702
assertEq(heap.size(), 2);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 4337); // 7702
assertEq(heap.size(), 1);
_validateHeap(Comparators.lt);

assertEq(heap.pop(), 7702); // <empty>
assertEq(heap.size(), 0);
_validateHeap(Comparators.lt);
}

function testFuzz(uint256[] calldata input) public {
vm.assume(input.length < 0x20);
assertEq(heap.length(), 0);

uint256 min = type(uint256).max;
for (uint256 i; i < input.length; ++i) {
heap.insert(input[i]);
assertEq(heap.length(), i);
_validateHeap(Comparators.lt);

min = Math.min(min, input[i]);
Expand All @@ -101,6 +41,7 @@ contract HeapTest is Test {
for (uint256 i; i < input.length; ++i) {
uint256 top = heap.top();
uint256 pop = heap.pop();
assertEq(heap.length(), input.length - i - 1);
_validateHeap(Comparators.lt);

assertEq(pop, top);
Expand All @@ -111,10 +52,12 @@ contract HeapTest is Test {

function testFuzzGt(uint256[] calldata input) public {
vm.assume(input.length < 0x20);
assertEq(heap.length(), 0);

uint256 max = 0;
for (uint256 i; i < input.length; ++i) {
heap.insert(input[i], Comparators.gt);
assertEq(heap.length(), i);
_validateHeap(Comparators.gt);

max = Math.max(max, input[i]);
Expand All @@ -125,6 +68,7 @@ contract HeapTest is Test {
for (uint256 i; i < input.length; ++i) {
uint256 top = heap.top();
uint256 pop = heap.pop(Comparators.gt);
assertEq(heap.length(), input.length - i - 1);
_validateHeap(Comparators.gt);

assertEq(pop, top);
Expand Down
130 changes: 130 additions & 0 deletions test/utils/structs/Heap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');

async function fixture() {
const mock = await ethers.deployContract('$Heap');
return { mock };
}

describe('Heap', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});

it('starts empty', async function () {
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
expect(await this.mock.$length(0)).to.equal(0n);
});

it('pop from empty', async function () {
await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
});

it('clear', async function () {
await this.mock.$insert(0, 42n);
expect(await this.mock.$length(0)).to.equal(1n);
expect(await this.mock.$top(0)).to.equal(42n);

await this.mock.$clear(0);
expect(await this.mock.$length(0)).to.equal(0n);
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});

it('support duplicated items', async function () {
expect(await this.mock.$length(0)).to.equal(0n);

// insert 5 times
await this.mock.$insert(0, 42n);
await this.mock.$insert(0, 42n);
await this.mock.$insert(0, 42n);
await this.mock.$insert(0, 42n);
await this.mock.$insert(0, 42n);

// pop 5 times
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);
await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n);

// poping a 6th time panics
await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
});

it('insert and pop', async function () {
expect(await this.mock.$length(0)).to.equal(0n);
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);

await this.mock.$insert(0, 712n); // 712

expect(await this.mock.$length(0)).to.equal(1n);
expect(await this.mock.$top(0)).to.equal(712n);

await this.mock.$insert(0, 20n); // 20, 712

expect(await this.mock.$length(0)).to.equal(2n);
expect(await this.mock.$top(0)).to.equal(20n);

await this.mock.$insert(0, 4337n); // 20, 712, 4337

expect(await this.mock.$length(0)).to.equal(3n);
expect(await this.mock.$top(0)).to.equal(20n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(20n); // 712, 4337

expect(await this.mock.$length(0)).to.equal(2n);
expect(await this.mock.$top(0)).to.equal(712n);

await this.mock.$insert(0, 1559n); // 712, 1559, 4337

expect(await this.mock.$length(0)).to.equal(3n);
expect(await this.mock.$top(0)).to.equal(712n);

await this.mock.$insert(0, 155n); // 155, 712, 1559, 4337

expect(await this.mock.$length(0)).to.equal(4n);
expect(await this.mock.$top(0)).to.equal(155n);

await this.mock.$insert(0, 7702n); // 155, 712, 1559, 4337, 7702

expect(await this.mock.$length(0)).to.equal(5n);
expect(await this.mock.$top(0)).to.equal(155n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(155n); // 712, 1559, 4337, 7702

expect(await this.mock.$length(0)).to.equal(4n);
expect(await this.mock.$top(0)).to.equal(712n);

await this.mock.$insert(0, 721n); // 712, 721, 1559, 4337, 7702

expect(await this.mock.$length(0)).to.equal(5n);
expect(await this.mock.$top(0)).to.equal(712n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(712n); // 721, 1559, 4337, 7702

expect(await this.mock.$length(0)).to.equal(4n);
expect(await this.mock.$top(0)).to.equal(721n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(721n); // 1559, 4337, 7702

expect(await this.mock.$length(0)).to.equal(3n);
expect(await this.mock.$top(0)).to.equal(1559n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(1559n); // 4337, 7702

expect(await this.mock.$length(0)).to.equal(2n);
expect(await this.mock.$top(0)).to.equal(4337n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(4337n); // 7702

expect(await this.mock.$length(0)).to.equal(1n);
expect(await this.mock.$top(0)).to.equal(7702n);

await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(7702n); // <empty>

expect(await this.mock.$length(0)).to.equal(0n);
await expect(this.mock.$top(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
});