From a960b5c52ebe8ac2706c47f6f6708afc7df8439c Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 18 Sep 2026 15:32:23 +0000 Subject: [PATCH 1/3] test: read layout values from LibMemoryKV, part 1 Co-Authored-By: Claude Opus 5 (1M context) --- test/lib/LibMemoryKVHandle.t.sol | 4 +- test/src/lib/LibMemoryKV.array.t.sol | 6 +- test/src/lib/LibMemoryKV.bisect.t.sol | 70 ++++++++------- test/src/lib/LibMemoryKV.crossFunction.t.sol | 27 +++--- test/src/lib/LibMemoryKV.exportWalk.t.sol | 25 +++--- test/src/lib/LibMemoryKV.gasClaims.t.sol | 23 +++-- test/src/lib/LibMemoryKV.getset.t.sol | 52 ++++++----- test/src/lib/LibMemoryKV.setCapacity.t.sol | 5 +- test/src/lib/LibMemoryKV.setSlotMask.t.sol | 40 +++++---- test/src/lib/LibMemoryKV.types.t.sol | 51 ++++++----- .../lib/LibMemoryKV.wordCountOverflow.t.sol | 89 ++++++++++--------- 11 files changed, 208 insertions(+), 184 deletions(-) diff --git a/test/lib/LibMemoryKVHandle.t.sol b/test/lib/LibMemoryKVHandle.t.sol index 2eb00f9..61fe8af 100644 --- a/test/lib/LibMemoryKVHandle.t.sol +++ b/test/lib/LibMemoryKVHandle.t.sol @@ -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++) { diff --git a/test/src/lib/LibMemoryKV.array.t.sol b/test/src/lib/LibMemoryKV.array.t.sol index 395f190..a354e30 100644 --- a/test/src/lib/LibMemoryKV.array.t.sol +++ b/test/src/lib/LibMemoryKV.array.t.sol @@ -158,9 +158,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"); } diff --git a/test/src/lib/LibMemoryKV.bisect.t.sol b/test/src/lib/LibMemoryKV.bisect.t.sol index 14fe16f..af11769 100644 --- a/test/src/lib/LibMemoryKV.bisect.t.sol +++ b/test/src/lib/LibMemoryKV.bisect.t.sol @@ -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 @@ -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; @@ -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 @@ -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); @@ -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); @@ -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); } @@ -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); } @@ -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); } @@ -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; @@ -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 @@ -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++) { diff --git a/test/src/lib/LibMemoryKV.crossFunction.t.sol b/test/src/lib/LibMemoryKV.crossFunction.t.sol index 50ecbb6..ca7fe7c 100644 --- a/test/src/lib/LibMemoryKV.crossFunction.t.sol +++ b/test/src/lib/LibMemoryKV.crossFunction.t.sol @@ -17,18 +17,15 @@ import {assertValue} from "test/lib/LibMemoryKVAssert.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 the saturated store below is built. Above the decoded `keys` - /// argument so the nodes cannot land on it, and low enough that fifteen - /// nodes all fit under the `0xFFFF` head pointer bound. + /// argument so the nodes cannot land on it, and low enough that + /// `LibMemoryKV.LIST_COUNT` nodes all fit under the + /// `LibMemoryKV.POINTER_MASK` head pointer bound. uint256 internal constant SATURATED_BASE = 0x300; - /// Build a store holding one key in each of the fifteen internal lists, - /// from a free memory pointer this frame fixes, and hand back only the - /// handle. The nodes are gone when this returns. + /// Build a store holding one key in each of the `LibMemoryKV.LIST_COUNT` + /// internal lists, from a free memory pointer this frame fixes, and hand + /// back only the handle. The nodes are gone when this returns. function buildSaturatedExternal(bytes32[] memory keys) external pure returns (MemoryKV) { assembly ("memory-safe") { mstore(0x40, SATURATED_BASE) @@ -40,18 +37,18 @@ contract LibMemoryKVCrossFunctionTest is Test { return kv; } - /// 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); diff --git a/test/src/lib/LibMemoryKV.exportWalk.t.sol b/test/src/lib/LibMemoryKV.exportWalk.t.sol index e4a8e32..88fcc62 100644 --- a/test/src/lib/LibMemoryKV.exportWalk.t.sol +++ b/test/src/lib/LibMemoryKV.exportWalk.t.sol @@ -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)); @@ -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); diff --git a/test/src/lib/LibMemoryKV.gasClaims.t.sol b/test/src/lib/LibMemoryKV.gasClaims.t.sol index a104065..f30a502 100644 --- a/test/src/lib/LibMemoryKV.gasClaims.t.sol +++ b/test/src/lib/LibMemoryKV.gasClaims.t.sol @@ -14,13 +14,10 @@ import {LibMemoryKVSlow} from "test/lib/LibMemoryKVSlow.sol"; /// the skip guards that produce the saving are invisible to a test that only /// reads the exported pairs. contract LibMemoryKVGasClaimsTest is Test { - /// `kv` carries one pointer per internal linked list. - uint256 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 constant SHALLOW_SLOT = 0x0e; + uint256 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 @@ -52,9 +49,9 @@ contract LibMemoryKVGasClaimsTest is Test { /// names the fourth. uint256 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 constant COLLIDING_SLOT = 3; /// What the README's `~` is read as here. @@ -111,19 +108,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 { @@ -133,7 +130,7 @@ contract LibMemoryKVGasClaimsTest is Test { } /// The naive linear loop the NatSpec measures the saving against is - /// `toBytes32ArrayLinear`, which visits all 15 slots and produces the same + /// `toBytes32ArrayLinear`, which visits every list and produces the same /// pairs. Measuring the linear walk first leaves the bisect allocating higher /// in memory, so the saving asserted here is the pessimistic one. function testExportGasBeatsLinearWalk() public view { diff --git a/test/src/lib/LibMemoryKV.getset.t.sol b/test/src/lib/LibMemoryKV.getset.t.sol index b3eb996..65fd373 100644 --- a/test/src/lib/LibMemoryKV.getset.t.sol +++ b/test/src/lib/LibMemoryKV.getset.t.sol @@ -15,9 +15,9 @@ import {headOf, lengthOf} from "test/lib/LibMemoryKVHandle.sol"; /// `get` after `set` on the same handle reports what was set. An insert /// allocates exactly one node and makes its key readable, and an update of a /// present key allocates nothing and changes that key's value alone. A node's -/// head pointer must fit its sixteen bit slot: `set` reverts with -/// `MemoryKVOverflow` for one that does not, and `get` reads the node words -/// that lie above the bound. +/// head pointer must fit its `LibMemoryKV.SLOT_BITS` wide slot: `set` reverts +/// with `MemoryKVOverflow` for one that does not, and `get` reads the node +/// words that lie above the bound. contract LibMemoryKVGetSetTest is Test, SetAtFreePointer { /// Insert at an exact free memory pointer and read the key back inside the /// SAME call frame, as the node only exists in that frame's memory. @@ -44,43 +44,47 @@ contract LibMemoryKVGetSetTest is Test, SetAtFreePointer { return LibMemoryKV.get(kv, first); } - /// `0xFFFF` is the widest head pointer a slot holds, so an insert whose node - /// starts exactly there succeeds and the slot encodes `0xFFFF`. + /// `LibMemoryKV.POINTER_MASK` is the widest head pointer a slot holds, so an + /// insert whose node starts exactly there succeeds and the slot encodes + /// `LibMemoryKV.POINTER_MASK`. function testSetPointerBoundaryMaxAccepted(MemoryKVKey key, MemoryKVVal value) external view { - MemoryKV kv = this.setAtFreePointer(MEMORY_KV_EMPTY, key, value, 0xFFFF); + MemoryKV kv = this.setAtFreePointer(MEMORY_KV_EMPTY, key, value, LibMemoryKV.POINTER_MASK); - assertEq(headOf(kv, key), 0xFFFF, "max pointer 0xFFFF must be encoded into this key's slot"); + assertEq(headOf(kv, key), LibMemoryKV.POINTER_MASK, "the max pointer must be encoded into this key's slot"); assertEq(lengthOf(kv), 2, "length"); } - /// The first pointer past the bound, `0x10000`, reverts `MemoryKVOverflow` - /// carrying that pointer, not the bound it crossed. + /// The first pointer past the bound, `LibMemoryKV.POINTER_MASK + 1`, reverts + /// `MemoryKVOverflow` carrying that pointer, not the bound it crossed. function testSetPointerBoundaryOverflowReverts(MemoryKVKey key, MemoryKVVal value) external { - vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVOverflow.selector, 0x10000)); - this.setAtFreePointer(MEMORY_KV_EMPTY, key, value, 0x10000); + vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVOverflow.selector, LibMemoryKV.POINTER_MASK + 1)); + this.setAtFreePointer(MEMORY_KV_EMPTY, key, value, LibMemoryKV.POINTER_MASK + 1); } /// The bound is on the node's HEAD pointer alone. A node inserted at the - /// maximum head pointer `0xFFFF` holds its value word at `0x1001F`, above - /// the bound, and `get` reads it: node fields are reached by full width - /// arithmetic from the head, not through 16 bit pointers. A `get` that - /// truncated a field address to 16 bits is indistinguishable from a - /// correct one for every node that fits entirely under the bound. + /// maximum head pointer `LibMemoryKV.POINTER_MASK` holds its value word at + /// `LibMemoryKV.POINTER_MASK + 0x20`, above the bound, and `get` reads it: + /// node fields are reached by full width arithmetic from the head, not + /// through `LibMemoryKV.SLOT_BITS` wide pointers. A `get` that truncated a + /// field address to `LibMemoryKV.SLOT_BITS` bits is indistinguishable from + /// a correct one for every node that fits entirely under the bound. function testGetReadsAValueWordAboveTheBound(MemoryKVKey key, MemoryKVVal value) external view { - (uint256 exists, MemoryKVVal got) = this.setAtPointerThenGetExternal(key, value, 0xFFFF); + (uint256 exists, MemoryKVVal got) = this.setAtPointerThenGetExternal(key, value, LibMemoryKV.POINTER_MASK); assertEq(exists, 1, "a node at the maximum head pointer exists"); - assertEq(MemoryKVVal.unwrap(got), MemoryKVVal.unwrap(value), "the value word above 0xFFFF reads back"); + assertEq(MemoryKVVal.unwrap(got), MemoryKVVal.unwrap(value), "the value word above the bound reads back"); } - /// The same for the next pointer word. The second node's head is `0xFFC0`, - /// which fits the bound, so its next word lands at exactly `0x10000` and - /// the walk from that node down to the first one has to read across the - /// bound to find it. + /// The same for the next pointer word. The second node's head is + /// `LibMemoryKV.POINTER_MASK + 1 - 0x40`, which fits the bound, so its next + /// word lands at exactly `LibMemoryKV.POINTER_MASK + 1` and the walk from + /// that node down to the first one has to read across the bound to find it. function testGetWalksThroughANextWordAboveTheBound(MemoryKVKey key, MemoryKVVal value) external view { - (uint256 exists, MemoryKVVal got) = this.insertPairThenGetFirstExternal(key, collidingKey(key), value, 0xFFC0); + (uint256 exists, MemoryKVVal got) = this.insertPairThenGetFirstExternal( + key, collidingKey(key), value, LibMemoryKV.POINTER_MASK + 1 - 0x40 + ); - assertEq(exists, 1, "the walk reaches the first key through a next word at 0x10000"); + assertEq(exists, 1, "the walk reaches the first key through a next word above the bound"); assertEq(MemoryKVVal.unwrap(got), MemoryKVVal.unwrap(value), "the first key's value survives the walk"); } diff --git a/test/src/lib/LibMemoryKV.setCapacity.t.sol b/test/src/lib/LibMemoryKV.setCapacity.t.sol index a7354c1..b9bc3e4 100644 --- a/test/src/lib/LibMemoryKV.setCapacity.t.sol +++ b/test/src/lib/LibMemoryKV.setCapacity.t.sol @@ -6,6 +6,7 @@ import {Test} from "forge-std-1.16.2/src/Test.sol"; import {LibPointer, Pointer} from "rain-solmem-0.1.28/src/lib/LibPointer.sol"; import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal, MEMORY_KV_EMPTY} from "src/lib/LibMemoryKV.sol"; +import {lengthOf} from "test/lib/LibMemoryKVHandle.sol"; /// @title LibMemoryKVSetCapacityTest /// `set` can revert, and the ceiling it reverts at is the frame's free memory @@ -30,7 +31,7 @@ contract LibMemoryKVSetCapacityTest is Test { for (uint256 i = 1; i <= pairs; i++) { kv = kv.set(MemoryKVKey.wrap(bytes32(i)), MemoryKVVal.wrap(bytes32(i))); } - return MemoryKV.unwrap(kv) >> 0xf0; + return lengthOf(kv); } /// Allocate an unrelated `bytes32[]` of `elements` elements in a frame that @@ -54,7 +55,7 @@ contract LibMemoryKVSetCapacityTest is Test { for (uint256 i = 1; i <= pairs; i++) { kv = kv.set(MemoryKVKey.wrap(bytes32(i)), MemoryKVVal.wrap(bytes32(i))); } - return MemoryKV.unwrap(kv) >> 0xf0; + return lengthOf(kv); } /// Insert `key`, then move the free memory pointer far above the bound and diff --git a/test/src/lib/LibMemoryKV.setSlotMask.t.sol b/test/src/lib/LibMemoryKV.setSlotMask.t.sol index 3228ced..180bac6 100644 --- a/test/src/lib/LibMemoryKV.setSlotMask.t.sol +++ b/test/src/lib/LibMemoryKV.setSlotMask.t.sol @@ -11,9 +11,10 @@ import {headOf, lengthOf} from "test/lib/LibMemoryKVHandle.sol"; /// @title LibMemoryKVSetSlotMaskTest /// An insert clears its list's head slot and writes the new head into it, and -/// the region it clears is EXACTLY the sixteen bits of that slot: the new head -/// keeps no bit of the old one, neither the slot's low bit nor its high bit, -/// and the slot above keeps every bit of its own head, its low bit included. +/// the region it clears is EXACTLY the `LibMemoryKV.SLOT_BITS` bits of that +/// slot: the new head keeps no bit of the old one, neither the slot's low bit +/// nor its high bit, and the slot above keeps every bit of its own head, its +/// low bit included. /// /// A Solidity allocator places every node at a multiple of 32, and every newer /// node above every older one. The low bits of an old and a new head are then @@ -31,12 +32,13 @@ contract LibMemoryKVSetSlotMaskTest is Test, SetAtFreePointer { uint256 internal constant ODD_POINTER = 0x101; /// An even node address clear of the three words at `ODD_POINTER`, with - /// the high bit of a slot (`0x8000`) clear. + /// the high bit of a slot, `2 ** (LibMemoryKV.SLOT_BITS - 1)`, clear. uint256 internal constant EVEN_POINTER = 0x180; - /// An even node address with the high bit of a slot (`0x8000`) set, clear - /// of the three words at `EVEN_POINTER`. - uint256 internal constant HIGH_BIT_POINTER = 0x8100; + /// An even node address with the high bit of a slot, + /// `2 ** (LibMemoryKV.SLOT_BITS - 1)`, set, clear of the three words at + /// `EVEN_POINTER`. + uint256 internal constant HIGH_BIT_POINTER = 2 ** (LibMemoryKV.SLOT_BITS - 1) + 0x100; /// Insert `first` at `firstPointer`, then `second` at `secondPointer`, and /// read `first` back in the frame that owns both nodes. The nodes die with @@ -107,15 +109,17 @@ contract LibMemoryKVSetSlotMaskTest is Test, SetAtFreePointer { assertEq(uint256(value), 0xA100, "and still carries its value"); } - /// One key in list 14 at an odd address, then a key in list 13. Writing - /// list 13's head leaves list 14's head whole, its low bit (bit 224 of the - /// store) included, so list 14 is still headed at `ODD_POINTER`. + /// One key in the highest list, `LibMemoryKV.LIST_COUNT - 1`, at an odd + /// address, then a key in the list below it. Writing the lower list's head + /// leaves the highest list's head whole, its low bit (bit + /// `(LibMemoryKV.LIST_COUNT - 1) * LibMemoryKV.SLOT_BITS` of the store) + /// included, so the highest list is still headed at `ODD_POINTER`. /// - /// List 14 is the highest there is, so its head sits directly under the - /// word count and is the slot a rewrite is likeliest to reach past. + /// The highest list's head sits directly under the word count and is the + /// slot a rewrite is likeliest to reach past. function testInsertLeavesTheNeighbouringSlotsLowBit() external view { - MemoryKVKey high = keyForSlot(bytes32(uint256(1)), 14); - MemoryKVKey low = keyForSlot(bytes32(uint256(1)), 13); + MemoryKVKey high = keyForSlot(bytes32(uint256(1)), LibMemoryKV.LIST_COUNT - 1); + MemoryKVKey low = keyForSlot(bytes32(uint256(1)), LibMemoryKV.LIST_COUNT - 2); (MemoryKV kv, uint256 exists, bytes32 value) = this.insertAtTwoPointersExternal( high, @@ -126,10 +130,12 @@ contract LibMemoryKVSetSlotMaskTest is Test, SetAtFreePointer { EVEN_POINTER ); - assertEq(headOf(kv, 14), ODD_POINTER, "list 14 keeps the whole address it was given"); - assertEq(headOf(kv, 13), EVEN_POINTER, "list 13 heads its own node"); + assertEq( + headOf(kv, LibMemoryKV.LIST_COUNT - 1), ODD_POINTER, "the highest list keeps the whole address it was given" + ); + assertEq(headOf(kv, LibMemoryKV.LIST_COUNT - 2), EVEN_POINTER, "the list below it heads its own node"); assertEq(lengthOf(kv), 4, "two pairs is four words"); - assertEq(exists, 1, "the key in list 14 is still reachable"); + assertEq(exists, 1, "the key in the highest list is still reachable"); assertEq(uint256(value), 0xA100, "and still carries its value"); } } diff --git a/test/src/lib/LibMemoryKV.types.t.sol b/test/src/lib/LibMemoryKV.types.t.sol index 86bb3f4..61fc66d 100644 --- a/test/src/lib/LibMemoryKV.types.t.sol +++ b/test/src/lib/LibMemoryKV.types.t.sol @@ -7,18 +7,17 @@ import {Test} from "forge-std-1.16.2/src/Test.sol"; import {LibMemoryKV, MemoryKV, MemoryKVKey, MemoryKVVal, MEMORY_KV_EMPTY} from "src/lib/LibMemoryKV.sol"; import {collidingPairDifferingInBit} from "test/lib/LibMemoryKVKeys.sol"; import {countPair} from "test/lib/LibMemoryKVExport.sol"; +import {COUNT_MAX, headOf, lengthOf} from "test/lib/LibMemoryKVHandle.sol"; /// @title LibMemoryKVTypesTest /// The declarations the rest of the suite is written on top of: the one word an -/// empty store is, and the full width of the key and value it carries. Every -/// other test reads these three calls downstream, where a wrong empty store -/// arrives as a wrong export or a wrong walk. Here they are the word itself. +/// empty store is, the layout that packs the head pointers and the word count +/// into a word, and the full width of the key and value a store carries. Every +/// other test reads these downstream, where a wrong one arrives as a wrong +/// export or a wrong walk. Here they are checked on the word itself. contract LibMemoryKVTypesTest is Test { using LibMemoryKV for MemoryKV; - /// One head pointer per internal linked list. - uint256 internal constant SLOTS = 15; - /// The bit a key keeps that no random fuzz pair differs in alone. uint256 internal constant TOP_BIT = 0xff; @@ -28,7 +27,7 @@ contract LibMemoryKVTypesTest is Test { MemoryKV kv = MEMORY_KV_EMPTY.set(low, MemoryKVVal.wrap(bytes32(uint256(0xA)))); kv = kv.set(high, MemoryKVVal.wrap(bytes32(uint256(0xB)))); - assertEq(MemoryKV.unwrap(kv) >> 0xf0, 4, "two pairs counted"); + assertEq(lengthOf(kv), 4, "two pairs counted"); (uint256 lowExists, MemoryKVVal lowValue) = kv.get(low); assertEq(lowExists, 1, "low key exists"); @@ -49,23 +48,35 @@ contract LibMemoryKVTypesTest is Test { assertEq(MemoryKV.unwrap(MEMORY_KV_EMPTY), 0); } - /// The word count is the top 16 bits of the store, and an empty store has - /// counted nothing. + /// The library's layout constants tile the word: the head pointer slots + /// fill the bits below the count, the count is the one slot above them, and + /// `LibMemoryKV.POINTER_MASK`, the widest head pointer, and `COUNT_MAX`, the + /// widest count, are each exactly one slot wide. + function testLayoutConstantsTileTheWord() external pure { + assertEq( + LibMemoryKV.COUNT_BIT_OFFSET, + LibMemoryKV.LIST_COUNT * LibMemoryKV.SLOT_BITS, + "the count sits directly above the last list" + ); + assertEq(LibMemoryKV.COUNT_BIT_OFFSET + LibMemoryKV.SLOT_BITS, 256, "the count is the top slot of the word"); + assertEq(LibMemoryKV.POINTER_MASK, 2 ** LibMemoryKV.SLOT_BITS - 1, "a pointer is one slot wide"); + assertEq(COUNT_MAX, LibMemoryKV.POINTER_MASK, "the count is one slot wide"); + } + + /// The word count is the slot at `LibMemoryKV.COUNT_BIT_OFFSET`, the top of + /// the store, and an empty store has counted nothing. function testEmptyStoreHasNoWordCount() external pure { - assertEq(MemoryKV.unwrap(MEMORY_KV_EMPTY) >> 0xf0, 0); + assertEq(lengthOf(MEMORY_KV_EMPTY), 0); } - /// The 15 head pointers are the 240 bits below the count, 16 bits each, and - /// every one of them is empty. Slot 14 is the one that abuts the count, so - /// a count wider than 16 bits would read here as a pointer that is not - /// there. + /// The `LibMemoryKV.LIST_COUNT` head pointers are the slots below the + /// count, `LibMemoryKV.SLOT_BITS` bits each, and every one of them is + /// empty. Slot `LibMemoryKV.LIST_COUNT - 1` is the one that abuts the + /// count, so a count wider than `LibMemoryKV.SLOT_BITS` would read here as a + /// pointer that is not there. function testEmptyStoreHasNoHeadPointerInAnySlot() external pure { - for (uint256 slot = 0; slot < SLOTS; slot++) { - assertEq( - (MemoryKV.unwrap(MEMORY_KV_EMPTY) >> (slot * 0x10)) & 0xFFFF, - 0, - string.concat("slot ", vm.toString(slot)) - ); + for (uint256 slot = 0; slot < LibMemoryKV.LIST_COUNT; slot++) { + assertEq(headOf(MEMORY_KV_EMPTY, slot), 0, string.concat("slot ", vm.toString(slot))); } } diff --git a/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol b/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol index 59fba0f..cb183f3 100644 --- a/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol +++ b/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol @@ -10,21 +10,22 @@ import {COUNT_MAX, lengthOf, headOf, withCount} from "test/lib/LibMemoryKVHandle import {setFreePointer} from "test/lib/LibFreeMemory.sol"; /// @title LibMemoryKVWordCountOverflowTest -/// The word count is SIXTEEN bits and an insert adds two to it, so there is a -/// count from which one more insert does not fit. The count is written by -/// shifting into the top of `MemoryKV`, which drops whatever does not fit, and -/// `toBytes32Array` sizes its allocation from the count and then copies every -/// pair it walks -- so a count that lost its high bit is a short allocation -/// written past, not a wrong number a caller might notice. +/// The word count is ONE SLOT, `LibMemoryKV.SLOT_BITS` wide, and an insert adds +/// two to it, so there is a count from which one more insert does not fit. The +/// count is written by shifting into the top of `MemoryKV`, which drops +/// whatever does not fit, and `toBytes32Array` sizes its allocation from the +/// count and then copies every pair it walks -- so a count that lost its high +/// bit is a short allocation written past, not a wrong number a caller might +/// notice. /// /// A store built only through `set` cannot get there, because a node address -/// must fit sixteen bits too and that runs out first. That is a bound on -/// addresses, not on the count, and it is the pointer width that sets it. These -/// assert the count is bounded by a bound on the count. +/// must fit a slot too and that runs out first. That is a bound on addresses, +/// not on the count, and it is the pointer width that sets it. These assert the +/// count is bounded by a bound on the count. /// /// Both bounds are read by one comparison over `pointer | length`, so where the -/// two values meet at `0xFFFF` is here too: what that comparison still accepts, -/// and which of the two an overflow is reported as. +/// two values meet at `LibMemoryKV.POINTER_MASK` is here too: what that +/// comparison still accepts, and which of the two an overflow is reported as. contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { /// Somewhere low enough that the inserted node's address cannot be what /// overflows, and clear of the scratch space and the free memory pointer. @@ -48,13 +49,14 @@ contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { return (kv, exists, MemoryKVVal.unwrap(value)); } - /// `0xFFFE + 2` is `0x10000`, which is one bit wider than the field. Shifted - /// into place that bit is gone and the count reads back as `0`, so the - /// insert that does not fit is the one that reports an empty store. + /// `COUNT_MAX - 1 + 2` is `COUNT_MAX + 1`, which is one bit wider than the + /// field. Shifted into place that bit is gone and the count reads back as + /// `0`, so the insert that does not fit is the one that reports an empty + /// store. function testSetRevertsRatherThanWrappingTheWordCountToZero() external { - vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVLengthOverflow.selector, 0x10000)); + vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVLengthOverflow.selector, COUNT_MAX + 1)); this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFE), + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 1), MemoryKVKey.wrap(bytes32(uint256(1))), MemoryKVVal.wrap(bytes32(uint256(2))), LOW_FREE_POINTER @@ -75,16 +77,16 @@ contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { ); } - /// The last insert that fits still happens. `0xFFFC + 2` is `0xFFFE`, the - /// widest even count the field holds, and it must come back intact rather - /// than be refused a step early. + /// The last insert that fits still happens. `COUNT_MAX - 3 + 2` is + /// `COUNT_MAX - 1`, the widest even count the field holds, and it must come + /// back intact rather than be refused a step early. function testSetAcceptsTheWidestWordCountThatFits() external view { MemoryKVKey key = MemoryKVKey.wrap(bytes32(uint256(1))); MemoryKV kv = this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFC), key, MemoryKVVal.wrap(bytes32(uint256(2))), LOW_FREE_POINTER + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 3), key, MemoryKVVal.wrap(bytes32(uint256(2))), LOW_FREE_POINTER ); - assertEq(lengthOf(kv), 0xFFFE, "the widest count that fits is written whole"); + assertEq(lengthOf(kv), COUNT_MAX - 1, "the widest count that fits is written whole"); assertEq(headOf(kv, key), LOW_FREE_POINTER, "the node is still recorded"); } @@ -112,7 +114,7 @@ contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { function testSetReportsThePointerWhenBothOverflow() external { vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVOverflow.selector, 0x12345)); this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFE), + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 1), MemoryKVKey.wrap(bytes32(uint256(1))), MemoryKVVal.wrap(bytes32(uint256(2))), 0x12345 @@ -120,48 +122,51 @@ contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { } /// `MemoryKVLengthOverflow` is documented for a count pushed "past - /// `0xFFFF`", so `0xFFFF` itself still fits and must be written whole. It is - /// also the widest value the shared comparison accepts, reached here with - /// both of its inputs nonzero, so a guard that added the node address and - /// the count rather than ORing their bits would refuse this insert. + /// `POINTER_MASK`", so `COUNT_MAX`, which is that bound, still fits and + /// must be written whole. It is also the widest value the shared comparison + /// accepts, reached here with both of its inputs nonzero, so a guard that + /// added the node address and the count rather than ORing their bits would + /// refuse this insert. function testSetAcceptsAWordCountOfExactlyTheBound() external view { MemoryKVKey key = MemoryKVKey.wrap(bytes32(uint256(1))); MemoryKV kv = this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFD), key, MemoryKVVal.wrap(bytes32(uint256(2))), LOW_FREE_POINTER + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 2), key, MemoryKVVal.wrap(bytes32(uint256(2))), LOW_FREE_POINTER ); - assertEq(lengthOf(kv), 0xFFFF, "a count of exactly the bound is written whole"); + assertEq(lengthOf(kv), COUNT_MAX, "a count of exactly the bound is written whole"); assertEq(headOf(kv, key), LOW_FREE_POINTER, "the node is still recorded"); } - /// `MemoryKVOverflow` is documented for a node address "above `0xFFFF`", so - /// a node landing exactly on `0xFFFF` has not overflowed and an overflowing - /// count alongside it is still reported as the count's error. Every other - /// input either leaves the guard alone or is already past the address bound, - /// so this is what separates an address bound of `above` from one of `at`. + /// `MemoryKVOverflow` is documented for a node "at a pointer above + /// `POINTER_MASK`", so a node landing exactly on `LibMemoryKV.POINTER_MASK` + /// has not overflowed and an overflowing count alongside it is still + /// reported as the count's error. Every other input either leaves the guard + /// alone or is already past the address bound, so this is what separates an + /// address bound of `above` from one of `at`. function testSetReportsTheCountWhenTheAddressIsTheWidestValidOne() external { - vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVLengthOverflow.selector, 0x10000)); + vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVLengthOverflow.selector, COUNT_MAX + 1)); this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFE), + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 1), MemoryKVKey.wrap(bytes32(uint256(1))), MemoryKVVal.wrap(bytes32(uint256(2))), - 0xFFFF + LibMemoryKV.POINTER_MASK ); } - /// The bound is `0xFFFF`, not the `0x10000` that first crosses it. With the - /// node address and the count both at `0x10000` their combined - /// `pointer | length` is `0x10000` as well, so a bound written one too high + /// The bound is `LibMemoryKV.POINTER_MASK`, not the one past it that first + /// crosses it. With the node address at `LibMemoryKV.POINTER_MASK + 1` and + /// the count at `COUNT_MAX + 1`, the same value, their combined + /// `pointer | length` is that value as well, so a bound written one too high /// lets this insert through entirely rather than merely naming the wrong /// overflow: `0x12345` above carries bits a raised bound still catches, and /// this carries none. function testSetOverflowsWhenBothValuesAreTheFirstInvalidOne() external { - vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVOverflow.selector, 0x10000)); + vm.expectRevert(abi.encodeWithSelector(LibMemoryKV.MemoryKVOverflow.selector, LibMemoryKV.POINTER_MASK + 1)); this.setAtFreePointer( - withCount(MEMORY_KV_EMPTY, 0xFFFE), + withCount(MEMORY_KV_EMPTY, COUNT_MAX - 1), MemoryKVKey.wrap(bytes32(uint256(1))), MemoryKVVal.wrap(bytes32(uint256(2))), - 0x10000 + LibMemoryKV.POINTER_MASK + 1 ); } From 34d2627eeb4e3974e0b112b5ce60b1864eb85150 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 18 Sep 2026 15:33:57 +0000 Subject: [PATCH 2/3] test: read layout values from LibMemoryKV, part 2 Co-Authored-By: Claude Opus 5 (1M context) --- test/src/lib/LibMemoryKV.getset.t.sol | 5 ++--- test/src/lib/LibMemoryKV.setInsert.t.sol | 5 +++-- test/src/lib/LibMemoryKV.types.t.sol | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/src/lib/LibMemoryKV.getset.t.sol b/test/src/lib/LibMemoryKV.getset.t.sol index 65fd373..baef65e 100644 --- a/test/src/lib/LibMemoryKV.getset.t.sol +++ b/test/src/lib/LibMemoryKV.getset.t.sol @@ -80,9 +80,8 @@ contract LibMemoryKVGetSetTest is Test, SetAtFreePointer { /// word lands at exactly `LibMemoryKV.POINTER_MASK + 1` and the walk from /// that node down to the first one has to read across the bound to find it. function testGetWalksThroughANextWordAboveTheBound(MemoryKVKey key, MemoryKVVal value) external view { - (uint256 exists, MemoryKVVal got) = this.insertPairThenGetFirstExternal( - key, collidingKey(key), value, LibMemoryKV.POINTER_MASK + 1 - 0x40 - ); + (uint256 exists, MemoryKVVal got) = + this.insertPairThenGetFirstExternal(key, collidingKey(key), value, LibMemoryKV.POINTER_MASK + 1 - 0x40); assertEq(exists, 1, "the walk reaches the first key through a next word above the bound"); assertEq(MemoryKVVal.unwrap(got), MemoryKVVal.unwrap(value), "the first key's value survives the walk"); diff --git a/test/src/lib/LibMemoryKV.setInsert.t.sol b/test/src/lib/LibMemoryKV.setInsert.t.sol index de5bcba..4599d87 100644 --- a/test/src/lib/LibMemoryKV.setInsert.t.sol +++ b/test/src/lib/LibMemoryKV.setInsert.t.sol @@ -155,8 +155,9 @@ contract LibMemoryKVSetInsertTest is Test, SetAtFreePointer { } /// The overflow error carries the OFFENDING pointer, not the bound it - /// crossed. `0x10000` is both the first invalid pointer and the value one - /// past the bound, so it cannot tell the two apart; `0x12345` can. + /// crossed. `LibMemoryKV.POINTER_MASK + 1` is both the first invalid + /// pointer and the value one past the bound, so it cannot tell the two + /// apart; `0x12345` can. function testSetOverflowPayloadIsTheOffendingPointerNotTheBound() external { MemoryKVKey key = MemoryKVKey.wrap(bytes32(uint256(1))); MemoryKVVal value = MemoryKVVal.wrap(bytes32(uint256(2))); diff --git a/test/src/lib/LibMemoryKV.types.t.sol b/test/src/lib/LibMemoryKV.types.t.sol index 61fc66d..40bb6a8 100644 --- a/test/src/lib/LibMemoryKV.types.t.sol +++ b/test/src/lib/LibMemoryKV.types.t.sol @@ -12,9 +12,8 @@ import {COUNT_MAX, headOf, lengthOf} from "test/lib/LibMemoryKVHandle.sol"; /// @title LibMemoryKVTypesTest /// The declarations the rest of the suite is written on top of: the one word an /// empty store is, the layout that packs the head pointers and the word count -/// into a word, and the full width of the key and value a store carries. Every -/// other test reads these downstream, where a wrong one arrives as a wrong -/// export or a wrong walk. Here they are checked on the word itself. +/// into a word, and the full width of the key and value a store carries. Here +/// they are checked on the word itself. contract LibMemoryKVTypesTest is Test { using LibMemoryKV for MemoryKV; From 60d9c203f0186a16aaea374ea6354cc122bb447a Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 18 Sep 2026 15:39:33 +0000 Subject: [PATCH 3/3] test: state the word count bound's fill from COUNT_MAX Co-Authored-By: Claude Opus 5 (1M context) --- test/src/lib/LibMemoryKV.wordCountOverflow.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol b/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol index cb183f3..8575862 100644 --- a/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol +++ b/test/src/lib/LibMemoryKV.wordCountOverflow.t.sol @@ -184,7 +184,7 @@ contract LibMemoryKVWordCountOverflowTest is Test, SetAtFreePointer { /// Every other test here forces the count with `withCount`, which is a /// `kv` no `set` produced. This is the one that asks what a caller who only /// ever calls `set` can reach, and the answer is that it is not this error: - /// carrying the count to `0x10000` needs 32768 inserts, and the node + /// a count past `COUNT_MAX` needs `(COUNT_MAX + 1) / 2` inserts, and the node /// address runs out 48 times earlier, at the 683rd, at `0x10040`. So /// `MemoryKVLengthOverflow` is not in a correct caller's reach and the /// header's claim above is a measurement rather than an assurance.