Skip to content
4 changes: 3 additions & 1 deletion test/lib/LibMemoryKVHandle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ contract LibMemoryKVHandleTest is Test {

/// The handle heads the named list with the named pointer, every other list
/// is empty, and the count is the one given.
function testHandleWithHeadsOnlyTheNamedList(uint256 slot, uint16 head, uint16 words) external pure {
function testHandleWithHeadsOnlyTheNamedList(uint256 slot, uint256 head, uint256 words) external pure {
slot = bound(slot, 0, LibMemoryKV.LIST_COUNT - 1);
head = bound(head, 0, LibMemoryKV.POINTER_MASK);
words = bound(words, 0, COUNT_MAX);
MemoryKV kv = handleWith(slot, head, words);

for (uint256 other = 0; other < LibMemoryKV.LIST_COUNT; other++) {
Expand Down
6 changes: 3 additions & 3 deletions test/src/lib/LibMemoryKV.array.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ contract LibMemoryKVArrayTest is Test {

// Counted over every pair at once, a pair exported twice pays for a
// pair not exported at all, so each pair is counted on its own. The
// linear walk names the pairs because it visits each of the 15 slots
// exactly once, so a list the bisect reads twice shows up as a linear
// pair matched twice.
// linear walk names the pairs because it visits each of the
// `LibMemoryKV.LIST_COUNT` slots exactly once, so a list the bisect
// reads twice shows up as a linear pair matched twice.
for (uint256 i = 0; i < arrayLinear.length; i += 2) {
assertEq(countPair(array, arrayLinear[i], arrayLinear[i + 1]), 1, "each pair exported exactly once");
}
Expand Down
70 changes: 38 additions & 32 deletions test/src/lib/LibMemoryKV.bisect.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import {setFreePointer, raiseFreePointerTo} from "test/lib/LibFreeMemory.sol";
///
/// The two halves are not mirror images, and the root split is the reason. The
/// low half is eight leaves under four interior nodes, routing slots 0..7 out
/// of `and(mask128, kv)`. The high half is seven, because the 16 bit length
/// field occupies the lane an eighth leaf would need: `shr(0x90, shl(0x10, kv))`
/// shifts the length out first, so `p0` lane j is slot 8+j for j in 0..6 and
/// lane 7 is structurally zero. The tree then collapses the node that would
/// have covered that zero lane — slot 14 is reached by `shr(0x20, p00)` with no
/// `and(mask16, ...)` scrub, where the mirroring leaf of the low half has one.
/// of `and(mask128, kv)`. The high half is seven, because the length field,
/// `LibMemoryKV.SLOT_BITS` wide, occupies the lane an eighth leaf would need:
/// `shr(0x90, shl(0x10, kv))` shifts the length out first, so `p0` lane j is
/// slot 8+j for j in 0..6 and lane 7 is structurally zero. The tree then
/// collapses the node that would have covered that zero lane — slot 14 is
/// reached by `shr(0x20, p00)` with no `and(mask16, ...)` scrub, where the
/// mirroring leaf of the low half has one.
/// That omission is sound only while the root split has already zeroed the
/// length, a coupling between two lines twelve apart, so the split and both
/// halves are pinned in one file rather than split across files that cannot see
Expand All @@ -39,17 +40,18 @@ import {setFreePointer, raiseFreePointerTo} from "test/lib/LibFreeMemory.sol";
/// The enumerations then run every combination of which slots are occupied.
/// That is the tree's whole input: each of its twenty eight guards tests a
/// window of `kv` against zero, and nothing it does branches on what a key or a
/// value is. Fifteen slots is 32768 combinations, so the enumerations cover the
/// named cases too, and nothing is sampled. The named cases are kept anyway,
/// for the name on the failure.
/// value is. `LibMemoryKV.LIST_COUNT` slots is `OCCUPANCY_COMBINATIONS`
/// combinations, so the enumerations cover the named cases too, and nothing is
/// sampled. The named cases are kept anyway, for the name on the failure.
///
/// `slotKeys` is what makes either affordable: one constant key per slot, so an
/// occupancy is chosen rather than searched for, and no test in this file takes
/// a fuzz input.
///
/// Every node of every case an enumeration builds is allocated from a free
/// memory pointer that is rewound between cases. Unrewound, the 32768 cases
/// walk that pointer past the `0xFFFF` ceiling `set` enforces after 115 of them.
/// memory pointer that is rewound between cases. Unrewound, the
/// `OCCUPANCY_COMBINATIONS` cases walk that pointer past the
/// `LibMemoryKV.POINTER_MASK` ceiling `set` enforces after 115 of them.
contract LibMemoryKVBisectTest is Test {
using LibMemoryKV for MemoryKV;

Expand All @@ -63,14 +65,15 @@ contract LibMemoryKVBisectTest is Test {
uint256 internal constant OCCUPANCY_LOW_HALF = 0x00FF;

/// Slots 8..14, the half the root split shifts the length out of.
uint256 internal constant OCCUPANCY_HIGH_HALF = 0x7F00;
uint256 internal constant OCCUPANCY_HIGH_HALF = OCCUPANCY_FULL ^ OCCUPANCY_LOW_HALF;

/// The top bit of a head pointer slot. A pointer is valid all the way to
/// `LibMemoryKV.POINTER_MASK`, so every mask and shift on the way down the
/// tree must carry this bit.
uint256 internal constant POINTER_HIGH_BIT = 2 ** (LibMemoryKV.SLOT_BITS - 1);

/// Stands for "no mask", which a 15 bit mask cannot collide with.
/// Stands for "no mask", which a `LibMemoryKV.LIST_COUNT` bit mask cannot
/// collide with.
uint256 internal constant NO_MASK = type(uint256).max;

/// Values sit above every key, so a copy that reads the key where the value
Expand Down Expand Up @@ -196,9 +199,9 @@ contract LibMemoryKVBisectTest is Test {
}

/// The store `mask` names, with every pointer in it at or above
/// `POINTER_HIGH_BIT`. A 16 bit pointer is valid all the way to
/// `POINTER_HIGH_BIT`. A pointer is valid all the way to
/// `LibMemoryKV.POINTER_MASK`, so every mask and shift on the way down must
/// carry bit 15.
/// carry that bit.
function checkNamedOccupancyFromHighPointers(uint256 mask) internal pure {
raiseFreePointerTo(POINTER_HIGH_BIT);

Expand All @@ -207,7 +210,7 @@ contract LibMemoryKVBisectTest is Test {
assertEq(occupancyOf(kv), mask, "exactly the named slots are populated");
for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) {
if (occupies(mask, slot)) {
assertGe(headOf(kv, slot), POINTER_HIGH_BIT, "pointer must have bit 15 set");
assertGe(headOf(kv, slot), POINTER_HIGH_BIT, "pointer must have POINTER_HIGH_BIT set");
}
}
checkExportedPairs(kv, mask, keys);
Expand Down Expand Up @@ -285,8 +288,9 @@ contract LibMemoryKVBisectTest is Test {
}

/// Slot 14 is the leaf the collapsed path reaches. Alone in the store its
/// 16 bits are the only nonzero bits of `p00`, so a shift of the wrong
/// distance yields a pointer into unwritten memory rather than the node.
/// `LibMemoryKV.SLOT_BITS` bits are the only nonzero bits of `p00`, so a
/// shift of the wrong distance yields a pointer into unwritten memory
/// rather than the node.
function testHighSlot14SoleExport() public pure {
checkSoleSlot(14);
}
Expand Down Expand Up @@ -360,7 +364,8 @@ contract LibMemoryKVBisectTest is Test {
checkNamedOccupancy(OCCUPANCY_HIGH_HALF);
}

/// Every slot of one half again, holding pointers with bit 15 set.
/// Every slot of one half again, holding pointers with
/// `POINTER_HIGH_BIT` set.
function testAllLowSlotsHighPointerExport() public pure {
checkNamedOccupancyFromHighPointers(OCCUPANCY_LOW_HALF);
}
Expand All @@ -369,8 +374,8 @@ contract LibMemoryKVBisectTest is Test {
checkNamedOccupancyFromHighPointers(OCCUPANCY_HIGH_HALF);
}

/// All fifteen slots at once. The two halves must be disjoint and must
/// between them reach every slot, so every pair appears exactly once.
/// Every slot at once. The two halves must be disjoint and must between
/// them reach every slot, so every pair appears exactly once.
function testEverySlotExportedExactlyOnce() public pure {
checkNamedOccupancy(OCCUPANCY_FULL);
}
Expand All @@ -389,7 +394,7 @@ contract LibMemoryKVBisectTest is Test {
///
/// The named tests above check the same thing through `countPair`, which
/// costs a scan per pair but names the slot it checked. Here the whole
/// point is 32768 cases, so the check is the cheap one.
/// point is `OCCUPANCY_COMBINATIONS` cases, so the check is the cheap one.
function exportMatches(bytes32[] memory array, uint256 mask, bytes32[] memory keys) internal pure returns (bool) {
if (array.length % 2 != 0) {
return false;
Expand All @@ -414,11 +419,11 @@ contract LibMemoryKVBisectTest is Test {
return found == mask;
}

/// Exports all 32768 occupancy combinations and checks each against the
/// pairs that combination must produce. The check is plain arithmetic and
/// the first mask that fails it is carried out of the loop, so the assertion
/// machinery runs once rather than 32768 times and the mask that failed is
/// what the assertion reports.
/// Exports all `OCCUPANCY_COMBINATIONS` occupancy combinations and checks
/// each against the pairs that combination must produce. The check is plain
/// arithmetic and the first mask that fails it is carried out of the loop,
/// so the assertion machinery runs once rather than once per combination and
/// the mask that failed is what the assertion reports.
///
/// Every case is built from one free memory pointer, `free`, taken after
/// `slotKeys` has allocated, so every node of every case lies in
Expand Down Expand Up @@ -449,21 +454,22 @@ contract LibMemoryKVBisectTest is Test {
assertEq(failed, NO_MASK, "occupancy mask whose export did not match");
}

/// The fifteen key constants are the whole reason a combination can be
/// The slot key constants are the whole reason a combination can be
/// chosen rather than searched for, and every test below reads its result
/// through the slot each key claims. Should the hash that `get` and `set`
/// share ever move, the keys stop naming the slots they claim and the tests
/// silently cover something other than what they report.
///
/// Occupying all fifteen at once is what makes them fifteen distinct slots
/// rather than fifteen keys that each hash somewhere. `set` routes on the
/// key alone, so that plus one key per slot is every occupancy.
/// Occupying all of them at once is what makes them
/// `LibMemoryKV.LIST_COUNT` distinct slots rather than that many keys that
/// each hash somewhere. `set` routes on the key alone, so that plus one key
/// per slot is every occupancy.
function testKeyConstantsLandWhereClaimed() public pure {
bytes32[] memory keys = slotKeys();
for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) {
assertEq(slotOf(keys[slot]), slot, "slot key hashes into its slot");
}
assertEq(occupancyOf(storeForOccupancy(OCCUPANCY_FULL, keys)), OCCUPANCY_FULL, "fifteen keys, fifteen slots");
assertEq(occupancyOf(storeForOccupancy(OCCUPANCY_FULL, keys)), OCCUPANCY_FULL, "every key in a slot of its own");

bytes32[12] memory chainKeys = slot0ChainKeys();
for (uint256 i = 0; i < chainKeys.length; i++) {
Expand Down
19 changes: 8 additions & 11 deletions test/src/lib/LibMemoryKV.crossFunction.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ import {setFreePointer} from "test/lib/LibFreeMemory.sol";
contract LibMemoryKVCrossFunctionTest is Test {
using LibMemoryKV for MemoryKV;

uint256 internal constant LIST_COUNT = 15;
uint256 internal constant SLOT_BITS = 0x10;
uint256 internal constant NODE_SIZE = 0x60;

/// Where `buildSaturatedExternal` builds its store. It must sit at or above
/// the free memory pointer after `keys` is decoded, which that function
/// checks on entry, so no node lands on `keys`. It is low enough that
/// fifteen nodes all sit at or under `POINTER_MASK`.
/// `LibMemoryKV.LIST_COUNT` nodes all sit at or under
/// `LibMemoryKV.POINTER_MASK`.
uint256 internal constant SATURATED_BASE = 0x300;

/// The free memory pointer on entry to a function in this contract, before
Expand Down Expand Up @@ -68,18 +65,18 @@ contract LibMemoryKVCrossFunctionTest is Test {
this.buildSaturatedExternal(tooMany);
}

/// A handle with every one of its sixteen fields in use arrives on the
/// other side of an external call bit for bit. The word it must equal is
/// A handle with every head pointer slot and the word count in use arrives
/// on the other side of an external call bit for bit. The word it must equal is
/// computed here from the addresses the building frame was forced to
/// allocate at, so a handle that lost the word count, or a slot, or the top
/// bit of one pointer, is a different number rather than a store that
/// merely reads oddly.
function testSaturatedHandleCrossesTheCallBoundaryBitForBit(bytes32 seed) external view {
bytes32[] memory keys = new bytes32[](LIST_COUNT);
uint256 expected = (LIST_COUNT * 2) << 0xf0;
for (uint256 slot = 0; slot < LIST_COUNT; slot++) {
bytes32[] memory keys = new bytes32[](LibMemoryKV.LIST_COUNT);
uint256 expected = (LibMemoryKV.LIST_COUNT * 2) << LibMemoryKV.COUNT_BIT_OFFSET;
for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) {
keys[slot] = MemoryKVKey.unwrap(keyForSlot(keccak256(abi.encode(seed, slot)), slot));
expected |= (SATURATED_BASE + slot * NODE_SIZE) << (slot * SLOT_BITS);
expected |= (SATURATED_BASE + slot * LibMemoryKV.NODE_BYTES) << (slot * LibMemoryKV.SLOT_BITS);
}

MemoryKV kv = this.buildSaturatedExternal(keys);
Expand Down
25 changes: 10 additions & 15 deletions test/src/lib/LibMemoryKV.exportWalk.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ import {setFreePointer} from "test/lib/LibFreeMemory.sol";
/// @title LibMemoryKVExportWalkTest
/// The export's WALK: following one internal list from its head to the
/// terminator. `MemoryKVOverflow` bounds the HEAD pointer of a list at
/// `0xFFFF` and nothing else, because "the node's three words MAY extend above
/// `0xFFFF`, as every field is reached by full width arithmetic from the head".
/// A head near the bound therefore keeps its next word above it, and the
/// export reads that word there at full width, exactly as `get` does, to reach
/// the pairs behind that node.
/// `LibMemoryKV.POINTER_MASK` and nothing else, because "the rest of the node
/// MAY extend above `POINTER_MASK`, as every field is reached by full width
/// arithmetic from the head". A head near the bound therefore keeps its next
/// word above it, and the export reads that word there at full width, exactly
/// as `get` does, to reach the pairs behind that node.
contract LibMemoryKVExportWalkTest is Test {
using LibMemoryKV for MemoryKV;

/// The free memory pointer the chain below is built from. The first node
/// lands here and the second, which is the one the list's slot points at,
/// `LibMemoryKV.NODE_BYTES` above it.
uint256 internal constant CHAIN_POINTER = 0xFF60;
uint256 internal constant CHAIN_POINTER = HEAD_POINTER - LibMemoryKV.NODE_BYTES;

/// Where the head node lands: the lowest head whose next word, at
/// `HEAD_POINTER + 0x40`, sits wholly above the bound. Every head above it
/// that `set` accepts, up to `LibMemoryKV.POINTER_MASK`, keeps its next
/// word higher still.
uint256 internal constant HEAD_POINTER = CHAIN_POINTER + LibMemoryKV.NODE_BYTES;
/// `HEAD_POINTER + 0x40`, sits wholly above the bound, as it starts at
/// `LibMemoryKV.POINTER_MASK + 1`. Every head above it that `set` accepts,
/// up to `LibMemoryKV.POINTER_MASK`, keeps its next word higher still.
uint256 internal constant HEAD_POINTER = LibMemoryKV.POINTER_MASK + 1 - 0x40;

/// Two keys that hash into one internal list.
bytes32 internal constant KEY_TAIL = bytes32(uint256(4));
Expand Down Expand Up @@ -59,11 +59,6 @@ contract LibMemoryKVExportWalkTest is Test {
/// `toBytes32Array` leaves it unspecified.
function testExportWalksThroughANextWordAboveTheBound() external view {
assertEq(slotOf(KEY_HEAD), slotOf(KEY_TAIL), "the two keys share one list");
assertEq(
HEAD_POINTER + 0x40,
LibMemoryKV.POINTER_MASK + 1,
"the head node's next word is the first word above the bound"
);

(MemoryKV kv, bytes32[] memory array) = this.exportChainAtExternal(CHAIN_POINTER);

Expand Down
21 changes: 9 additions & 12 deletions test/src/lib/LibMemoryKV.gasClaims.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@ import {setFreePointer} from "test/lib/LibFreeMemory.sol";
/// a get of the key furthest from its head, against the same with one key fewer
/// ahead of it.
contract LibMemoryKVGasClaimsTest is Test {
/// `kv` carries one pointer per internal linked list.
uint256 internal constant SLOTS = 0x0f;

/// The one slot the bisect reaches a level early. The length occupies the
/// high bits of `kv`, so stripping it leaves the last slot's pointer already
/// isolated and the tree tests it directly instead of descending to it.
uint256 internal constant SHALLOW_SLOT = 0x0e;
uint256 internal constant SHALLOW_SLOT = LibMemoryKV.LIST_COUNT - 1;

/// How far two equal-depth export paths may differ in gas. The optimizer
/// picks which arm of each conditional falls through, and a taken jump lands
Expand All @@ -34,9 +31,9 @@ contract LibMemoryKVGasClaimsTest is Test {
/// How many keys the colliding measurements put into one list.
uint256 internal constant COLLIDERS = 4;

/// The list the colliding measurements build. Any of the 15 would do: an
/// insert into an empty list and a get of a key alone in one cost the same
/// in every slot.
/// The list the colliding measurements build. Any list would do: an insert
/// into an empty list and a get of a key alone in one cost the same in
/// every slot.
uint256 internal constant COLLIDING_SLOT = 3;

/// Expands memory past anything the measurements below allocate, then
Expand Down Expand Up @@ -74,19 +71,19 @@ contract LibMemoryKVGasClaimsTest is Test {
/// `BRANCH_LAYOUT_GAS`, which shows up here as a slot that no longer matches
/// its peers.
function testExportGasIsUniformAcrossSlots() public view {
MemoryKV[] memory kvs = new MemoryKV[](SLOTS);
for (uint256 slot = 0; slot < SLOTS; slot++) {
MemoryKV[] memory kvs = new MemoryKV[](LibMemoryKV.LIST_COUNT);
for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) {
bytes32 key = MemoryKVKey.unwrap(keyForSlot(bytes32(slot + 1), slot));
kvs[slot] = LibMemoryKV.set(MEMORY_KV_EMPTY, MemoryKVKey.wrap(key), MemoryKVVal.wrap(bytes32(uint256(1))));
}

padMemory();
uint256[] memory gas = new uint256[](SLOTS);
for (uint256 slot = 0; slot < SLOTS; slot++) {
uint256[] memory gas = new uint256[](LibMemoryKV.LIST_COUNT);
for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) {
gas[slot] = bisectGas(kvs[slot]);
}

for (uint256 slot = 1; slot < SLOTS; slot++) {
for (uint256 slot = 1; slot < LibMemoryKV.LIST_COUNT; slot++) {
if (slot == SHALLOW_SLOT) {
assertLt(gas[slot], gas[0], "shallow slot must cost less");
} else {
Expand Down
Loading
Loading