diff --git a/src/lib/LibUint256Array.sol b/src/lib/LibUint256Array.sol index 2ef3ec1..cb3e060 100644 --- a/src/lib/LibUint256Array.sol +++ b/src/lib/LibUint256Array.sol @@ -3,53 +3,84 @@ pragma solidity ^0.8.25; import {Pointer} from "./LibPointer.sol"; +import {LibBytes32Array} from "./LibBytes32Array.sol"; +// `truncate` still reverts with `OutOfBoundsTruncate`, raised inside +// `LibBytes32Array`. The import is kept so that the symbol remains reachable +// from this file for downstream code that imports it from here, exactly as it +// was before this library was made a wrapper. +// forge-lint: disable-next-line(unused-import) import {OutOfBoundsTruncate} from "../error/ErrTruncate.sol"; /// @title LibUint256Array -/// @notice Things we want to do carefully and efficiently with uint256 arrays -/// that Solidity doesn't give us native tools for. +/// @notice The numeric view over `LibBytes32Array`. +/// +/// `uint256[]` and `bytes32[]` are the same structure in memory: a length word +/// followed by full width, 32 byte aligned items. Neither type has a cleanup +/// obligation on its items, because neither has any bits in a word that are not +/// its own. Relabelling one as the other is therefore a no-op on memory, and +/// the two libraries can share a single implementation instead of being kept in +/// sync by hand. +/// +/// `LibBytes32Array` holds that implementation and every function here +/// delegates to it, so the behavioural contract, the safety obligations on the +/// caller and the revert conditions are all defined there. This file adds +/// nothing but the relabel: read `LibBytes32Array` for what these functions +/// actually do. +/// +/// Every relabel is a bare `:=` in an inline assembly block at the point it is +/// needed. Each one copies a pointer between two identically shaped types and +/// touches no memory, so there is nothing for a helper to hoist except the +/// pointer copy itself. +/// +/// All of it is `internal`, so there is no call boundary between the two +/// libraries and the optimiser inlines the whole chain into the caller. library LibUint256Array { /// Pointer to the start (length prefix) of a `uint256[]`. + /// See `LibBytes32Array.startPointer`. /// @param array The array to get the start pointer of. /// @return pointer The pointer to the start of `array`. function startPointer(uint256[] memory array) internal pure returns (Pointer pointer) { + bytes32[] memory relabelled; assembly ("memory-safe") { - pointer := array + relabelled := array } + return LibBytes32Array.startPointer(relabelled); } /// Pointer to the data of a `uint256[]` NOT the length prefix. + /// See `LibBytes32Array.dataPointer`. /// @param array The array to get the data pointer of. /// @return pointer The pointer to the data of `array`. function dataPointer(uint256[] memory array) internal pure returns (Pointer pointer) { + bytes32[] memory relabelled; assembly ("memory-safe") { - pointer := add(array, 0x20) + relabelled := array } + return LibBytes32Array.dataPointer(relabelled); } /// Pointer to the end of the data of an array, i.e. one word past its last - /// item. - /// - /// This is derived from the CURRENT length word, so it is the end of the - /// allocated region only for an array that has not been shrunk. `truncate` - /// mutates the length word and leaks the tail, so after a truncation the - /// allocation extends beyond this pointer. + /// item. See `LibBytes32Array.endPointer` for what this does and does not + /// bound after a `truncate`. /// @param array The array to get the end pointer of. /// @return pointer The pointer to the end of the data of `array`. function endPointer(uint256[] memory array) internal pure returns (Pointer pointer) { + bytes32[] memory relabelled; assembly ("memory-safe") { - pointer := add(array, add(0x20, mul(0x20, mload(array)))) + relabelled := array } + return LibBytes32Array.endPointer(relabelled); } /// Cast a `Pointer` to `uint256[]` without modification or safety checks. /// The caller MUST ensure the pointer is to a valid region of memory for - /// some `uint256[]`. + /// some `uint256[]`. See `LibBytes32Array.unsafeAsBytes32Array`. /// @param pointer The pointer to cast to `uint256[]`. /// @return array The cast `uint256[]`. function unsafeAsUint256Array(Pointer pointer) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.unsafeAsBytes32Array(pointer); assembly ("memory-safe") { - array := pointer + array := relabelled } } @@ -58,11 +89,9 @@ library LibUint256Array { /// @param a A single integer to build an array around. /// @return array The newly allocated array including `a` as a single item. function arrayFrom(uint256 a) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 1) - mstore(add(array, 0x20), a) - mstore(0x40, add(array, 0x40)) + array := relabelled } } @@ -73,12 +102,9 @@ library LibUint256Array { /// @return array The newly allocated array including `a` and `b` as the only /// items. function arrayFrom(uint256 a, uint256 b) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a), bytes32(b)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 2) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(0x40, add(array, 0x60)) + array := relabelled } } @@ -90,13 +116,9 @@ library LibUint256Array { /// @return array The newly allocated array including `a`, `b` and `c` as the /// only items. function arrayFrom(uint256 a, uint256 b, uint256 c) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a), bytes32(b), bytes32(c)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 3) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(0x40, add(array, 0x80)) + array := relabelled } } @@ -109,14 +131,9 @@ library LibUint256Array { /// @return array The newly allocated array including `a`, `b`, `c` and `d` as the /// only items. function arrayFrom(uint256 a, uint256 b, uint256 c, uint256 d) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a), bytes32(b), bytes32(c), bytes32(d)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 4) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(add(array, 0x80), d) - mstore(0x40, add(array, 0xA0)) + array := relabelled } } @@ -134,15 +151,10 @@ library LibUint256Array { pure returns (uint256[] memory array) { + bytes32[] memory relabelled = + LibBytes32Array.arrayFrom(bytes32(a), bytes32(b), bytes32(c), bytes32(d), bytes32(e)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 5) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(add(array, 0x80), d) - mstore(add(array, 0xA0), e) - mstore(0x40, add(array, 0xC0)) + array := relabelled } } @@ -161,16 +173,10 @@ library LibUint256Array { pure returns (uint256[] memory array) { + bytes32[] memory relabelled = + LibBytes32Array.arrayFrom(bytes32(a), bytes32(b), bytes32(c), bytes32(d), bytes32(e), bytes32(f)); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 6) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(add(array, 0x80), d) - mstore(add(array, 0xA0), e) - mstore(add(array, 0xC0), f) - mstore(0x40, add(array, 0xE0)) + array := relabelled } } @@ -190,17 +196,11 @@ library LibUint256Array { pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom( + bytes32(a), bytes32(b), bytes32(c), bytes32(d), bytes32(e), bytes32(f), bytes32(g) + ); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 7) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(add(array, 0x80), d) - mstore(add(array, 0xA0), e) - mstore(add(array, 0xC0), f) - mstore(add(array, 0xE0), g) - mstore(0x40, add(array, 0x100)) + array := relabelled } } @@ -221,18 +221,11 @@ library LibUint256Array { pure returns (uint256[] memory array) { + bytes32[] memory relabelled = LibBytes32Array.arrayFrom( + bytes32(a), bytes32(b), bytes32(c), bytes32(d), bytes32(e), bytes32(f), bytes32(g), bytes32(h) + ); assembly ("memory-safe") { - array := mload(0x40) - mstore(array, 8) - mstore(add(array, 0x20), a) - mstore(add(array, 0x40), b) - mstore(add(array, 0x60), c) - mstore(add(array, 0x80), d) - mstore(add(array, 0xA0), e) - mstore(add(array, 0xC0), f) - mstore(add(array, 0xE0), g) - mstore(add(array, 0x100), h) - mstore(0x40, add(array, 0x120)) + array := relabelled } } @@ -242,25 +235,13 @@ library LibUint256Array { /// @param tail The tail of the new array. /// @return array The new array. function arrayFrom(uint256 a, uint256[] memory tail) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelledTail; assembly ("memory-safe") { - // Read the tail length ONCE, before anything is written into the - // output region. The output is allocated at the free memory - // pointer, so a tail at or above it overlaps the output and the - // writes below can land on the tail's own length word. Reading the - // length again after that would size the copy from a word the - // function itself just wrote, running it past the free memory - // pointer. - let tailLength := mload(tail) - let length := add(tailLength, 1) - let outputCursor := mload(0x40) - array := outputCursor - let outputEnd := add(outputCursor, add(0x20, mul(length, 0x20))) - mstore(0x40, outputEnd) - - mstore(outputCursor, length) - mstore(add(outputCursor, 0x20), a) - - mcopy(add(outputCursor, 0x40), add(tail, 0x20), mul(tailLength, 0x20)) + relabelledTail := tail + } + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a), relabelledTail); + assembly ("memory-safe") { + array := relabelled } } @@ -271,26 +252,13 @@ library LibUint256Array { /// @param tail The tail of the new array. /// @return array The new array. function arrayFrom(uint256 a, uint256 b, uint256[] memory tail) internal pure returns (uint256[] memory array) { + bytes32[] memory relabelledTail; assembly ("memory-safe") { - // Read the tail length ONCE, before anything is written into the - // output region. The output is allocated at the free memory - // pointer, so a tail at or above it overlaps the output and the - // writes below can land on the tail's own length word. Reading the - // length again after that would size the copy from a word the - // function itself just wrote, running it past the free memory - // pointer. - let tailLength := mload(tail) - let length := add(tailLength, 2) - let outputCursor := mload(0x40) - array := outputCursor - let outputEnd := add(outputCursor, add(0x20, mul(length, 0x20))) - mstore(0x40, outputEnd) - - mstore(outputCursor, length) - mstore(add(outputCursor, 0x20), a) - mstore(add(outputCursor, 0x40), b) - - mcopy(add(outputCursor, 0x60), add(tail, 0x20), mul(tailLength, 0x20)) + relabelledTail := tail + } + bytes32[] memory relabelled = LibBytes32Array.arrayFrom(bytes32(a), bytes32(b), relabelledTail); + assembly ("memory-safe") { + array := relabelled } } @@ -299,46 +267,26 @@ library LibUint256Array { /// are never deallocated. /// Reverts with `OutOfBoundsTruncate(array.length, newLength)` if /// `newLength` is greater than `array.length`. Truncation can only shrink. + /// See `LibBytes32Array.truncate`. /// @param array The array to truncate. MUTATED in place, so there is no /// return value and no new allocation. /// @param newLength The new length of `array` after truncation. MUST NOT /// be greater than `array.length`. function truncate(uint256[] memory array, uint256 newLength) internal pure { - if (newLength > array.length) { - revert OutOfBoundsTruncate(array.length, newLength); - } + bytes32[] memory relabelled; assembly ("memory-safe") { - mstore(array, newLength) + relabelled := array } + LibBytes32Array.truncate(relabelled, newLength); } - /// Extends `baseArray` with `extendArray` by allocating only an additional - /// `extendArray.length` words onto `baseArray` and copying only - /// `extendArray` if possible. If `baseArray` is large this MAY be - /// significantly more efficient than allocating - /// `baseArray.length + extendArray.length` for an entirely new array and - /// copying both `baseArray` and `extendArray` into the new array one item - /// at a time in Solidity. - /// - /// The efficient version of extension is only possible if the free memory - /// pointer sits at the end of the base array at the moment of extension. If - /// there is allocated memory after the end of base then extension will - /// require copying both the base and extend arrays to a new region of memory. - /// The caller is responsible for optimising code paths to avoid additional - /// allocations. + /// Extends `baseArray` with `extendArray`, allocating only when it must. /// /// This function is UNSAFE because the base array IS MUTATED DIRECTLY by /// some code paths AND THE FINAL RETURN ARRAY MAY POINT TO THE SAME REGION - /// OF MEMORY. It is NOT POSSIBLE to reliably see this behaviour from the - /// caller in all cases as the Solidity compiler optimisations may switch the - /// caller between the allocating and non-allocating logic due to subtle - /// optimisation reasons. To use this function safely THE CALLER MUST NOT USE - /// THE BASE ARRAY AND MUST USE THE RETURNED ARRAY ONLY. It is safe to use - /// the extend array after calling this function as it is never mutated, it - /// is only copied from. Extending an array by itself therefore always - /// allocates, as the in place path would rewrite the length word that base - /// and extend share. An uninitialised base also always allocates, as it - /// points at the permanently zero slot rather than at an allocation. + /// OF MEMORY. THE CALLER MUST NOT USE THE BASE ARRAY AND MUST USE THE + /// RETURNED ARRAY ONLY. See `LibBytes32Array.unsafeExtend` for the full + /// contract, including which shapes always allocate and why. /// /// Both arrays MUST be valid solidity memory arrays, each owning the region /// its own length word describes. @@ -351,77 +299,27 @@ library LibUint256Array { pure returns (uint256[] memory extended) { + bytes32[] memory relabelledBase; + bytes32[] memory relabelledExtend; assembly ("memory-safe") { - // Slither doesn't recognise assembly function names as mixed case - // even if they are. - // https://github.com/crytic/slither/issues/1815 - //slither-disable-next-line naming-convention - function extendInline(base, extend) -> baseAfter { - let outputCursor := mload(0x40) - let baseLength := mload(base) - let baseEnd := add(base, add(0x20, mul(baseLength, 0x20))) - - // The in place path rewrites base's length word where it sits. - // Below 0x80 there is no heap, only scratch space, the free - // memory pointer and the permanently zero slot that every - // uninitialised array points at, so a base there has no length - // word of its own to rewrite. When extend IS base that word is - // also extend's length word, so the write would mutate extend. - // The in place path is therefore only taken when base is a heap - // allocation that is the last thing in allocated memory AND - // extend is a different array. Otherwise allocate, copy and - // recurse. The copy puts base in the heap above every existing - // allocation, where it is both the last allocation and distinct - // from extend, so the recursion is one deep and lands on the in - // place path. - switch and(and(eq(outputCursor, baseEnd), iszero(lt(base, 0x80))), iszero(eq(base, extend))) - case 0 { - let newBase := outputCursor - // Base size includes the length word and is in bytes. - let newBaseSize := sub(baseEnd, base) - let newBaseEnd := add(newBase, newBaseSize) - mstore(0x40, newBaseEnd) - mcopy(newBase, base, newBaseSize) - - baseAfter := extendInline(newBase, extend) - } - case 1 { - // The extend length is read ONCE, before base's length word - // is overwritten, so the copy size never depends on the - // order of the read and the write. - let extendLength := mload(extend) - let totalLength := add(baseLength, extendLength) - let outputEnd := add(base, add(0x20, mul(totalLength, 0x20))) - mstore(base, totalLength) - mstore(0x40, outputEnd) - mcopy(baseEnd, add(extend, 0x20), mul(extendLength, 0x20)) - - baseAfter := base - } - } - - extended := extendInline(baseArray, extendArray) + relabelledBase := baseArray + relabelledExtend := extendArray + } + bytes32[] memory relabelled = LibBytes32Array.unsafeExtend(relabelledBase, relabelledExtend); + assembly ("memory-safe") { + extended := relabelled } } /// Reverse an array in place. This is a destructive operation that MUTATES /// the array in place. There is no return value. + /// See `LibBytes32Array.reverse`. /// @param array The array to reverse. function reverse(uint256[] memory array) internal pure { + bytes32[] memory relabelled; assembly ("memory-safe") { - for { - let left := add(array, 0x20) - // Right points at the last item in the array. Which is the - // length number of items from the length. - let right := add(array, mul(mload(array), 0x20)) - } lt(left, right) { - left := add(left, 0x20) - right := sub(right, 0x20) - } { - let leftValue := mload(left) - mstore(left, mload(right)) - mstore(right, leftValue) - } + relabelled := array } + LibBytes32Array.reverse(relabelled); } } diff --git a/src/lib/LibUint256Matrix.sol b/src/lib/LibUint256Matrix.sol index 08939bd..68224e5 100644 --- a/src/lib/LibUint256Matrix.sol +++ b/src/lib/LibUint256Matrix.sol @@ -5,63 +5,81 @@ pragma solidity ^0.8.25; // keeping this import here so downstream code can get LibPointer easily. // forge-lint: disable-next-line(unused-import) import {Pointer, LibPointer} from "./LibPointer.sol"; +import {LibBytes32Matrix} from "./LibBytes32Matrix.sol"; /// @title LibUint256Matrix -/// @notice Pointer access, literal construction and flattening for -/// `uint256[][]`. A `uint256[][]` is a length prefixed array of POINTERS to -/// `uint256[]`, not a contiguous block, so pointer arithmetic over a matrix -/// walks the references only and never the inner arrays. +/// @notice The numeric view over `LibBytes32Matrix`. +/// +/// `uint256[][]` and `bytes32[][]` are the same structure in memory: a length +/// word followed by full width pointers to the sub arrays, which are themselves +/// the same structure for both element types. Relabelling one as the other is +/// therefore a no-op on memory, and the two libraries can share a single +/// implementation instead of being kept in sync by hand. +/// +/// `LibBytes32Matrix` holds that implementation and every function here +/// delegates to it, so the behavioural contract, the safety obligations on the +/// caller and the revert conditions are all defined there. This file adds +/// nothing but the relabel: read `LibBytes32Matrix` for what these functions +/// actually do. +/// +/// Every relabel is a bare `:=` in an inline assembly block at the point it is +/// needed. Each one copies a pointer between two identically shaped types and +/// touches no memory, so there is nothing for a helper to hoist except the +/// pointer copy itself. +/// +/// All of it is `internal`, so there is no call boundary between the two +/// libraries and the optimiser inlines the whole chain into the caller. library LibUint256Matrix { /// Pointer to the start (length prefix) of a `uint256[][]`. + /// See `LibBytes32Matrix.startPointer`. /// @param matrix The matrix to get the start pointer of. /// @return pointer The pointer to the start of `matrix`. function startPointer(uint256[][] memory matrix) internal pure returns (Pointer pointer) { + bytes32[][] memory relabelled; assembly ("memory-safe") { - pointer := matrix + relabelled := matrix } + return LibBytes32Matrix.startPointer(relabelled); } /// Pointer to the data of a `uint256[][]` NOT the length prefix. /// Note that the data of a `uint256[][]` is _references_ to the `uint256[]` /// start pointers and does NOT include the arrays themselves. + /// See `LibBytes32Matrix.dataPointer`. /// @param matrix The matrix to get the data pointer of. /// @return pointer The pointer to the data of `matrix`. function dataPointer(uint256[][] memory matrix) internal pure returns (Pointer pointer) { + bytes32[][] memory relabelled; assembly ("memory-safe") { - pointer := add(matrix, 0x20) + relabelled := matrix } + return LibBytes32Matrix.dataPointer(relabelled); } /// Pointer to one word past the last `uint256[]` REFERENCE in a matrix, /// i.e. the end of the matrix's pointer array. /// - /// This is NOT the end of the memory allocated for the matrix. The data of a - /// `uint256[][]` is _references_ to the `uint256[]` start pointers and does - /// NOT include the arrays themselves. Those arrays are separate allocations - /// lying outside the pointer array entirely: above it for a matrix built by - /// `new uint256[][](n)`, below it for a matrix built by `matrixFrom` from - /// existing arrays. A matrix is therefore not one contiguous allocation and - /// no pointer marks the end of it. - /// - /// Allocating or writing at this pointer overwrites whatever sits above the - /// pointer array. For a matrix built by `new uint256[][](n)` that is the - /// inner arrays themselves, so it corrupts the matrix. + /// This is NOT the end of the memory allocated for the matrix, and writing + /// at it can corrupt the matrix. See `LibBytes32Matrix.endPointer` for why. /// @param matrix The matrix to get the end pointer of. /// @return pointer The pointer one word past the last reference in `matrix`. function endPointer(uint256[][] memory matrix) internal pure returns (Pointer pointer) { + bytes32[][] memory relabelled; assembly ("memory-safe") { - pointer := add(matrix, add(0x20, mul(0x20, mload(matrix)))) + relabelled := matrix } + return LibBytes32Matrix.endPointer(relabelled); } /// Cast a `Pointer` to `uint256[][]` without modification or safety checks. /// The caller MUST ensure the pointer is to a valid region of memory for - /// some `uint256[][]`. + /// some `uint256[][]`. See `LibBytes32Matrix.unsafeAsBytes32Matrix`. /// @param pointer The pointer to cast to `uint256[][]`. /// @return matrix The cast `uint256[][]`. function unsafeAsUint256Matrix(Pointer pointer) internal pure returns (uint256[][] memory matrix) { + bytes32[][] memory relabelled = LibBytes32Matrix.unsafeAsBytes32Matrix(pointer); assembly ("memory-safe") { - matrix := pointer + matrix := relabelled } } @@ -71,11 +89,13 @@ library LibUint256Matrix { /// @param a The 1-dimensional array to include in the matrix. /// @return matrix The 2-dimensional matrix containing `a`. function matrixFrom(uint256[] memory a) internal pure returns (uint256[][] memory matrix) { + bytes32[] memory relabelledA; + assembly ("memory-safe") { + relabelledA := a + } + bytes32[][] memory relabelled = LibBytes32Matrix.matrixFrom(relabelledA); assembly ("memory-safe") { - matrix := mload(0x40) - mstore(matrix, 1) - mstore(add(matrix, 0x20), a) - mstore(0x40, add(matrix, 0x40)) + matrix := relabelled } } @@ -86,12 +106,15 @@ library LibUint256Matrix { /// @param b Second 1-dimensional array to include in the matrix. /// @return matrix The 2-dimensional matrix containing `a` and `b`. function matrixFrom(uint256[] memory a, uint256[] memory b) internal pure returns (uint256[][] memory matrix) { + bytes32[] memory relabelledA; + bytes32[] memory relabelledB; assembly ("memory-safe") { - matrix := mload(0x40) - mstore(matrix, 2) - mstore(add(matrix, 0x20), a) - mstore(add(matrix, 0x40), b) - mstore(0x40, add(matrix, 0x60)) + relabelledA := a + relabelledB := b + } + bytes32[][] memory relabelled = LibBytes32Matrix.matrixFrom(relabelledA, relabelledB); + assembly ("memory-safe") { + matrix := relabelled } } @@ -107,13 +130,17 @@ library LibUint256Matrix { pure returns (uint256[][] memory matrix) { + bytes32[] memory relabelledA; + bytes32[] memory relabelledB; + bytes32[] memory relabelledC; + assembly ("memory-safe") { + relabelledA := a + relabelledB := b + relabelledC := c + } + bytes32[][] memory relabelled = LibBytes32Matrix.matrixFrom(relabelledA, relabelledB, relabelledC); assembly ("memory-safe") { - matrix := mload(0x40) - mstore(matrix, 3) - mstore(add(matrix, 0x20), a) - mstore(add(matrix, 0x40), b) - mstore(add(matrix, 0x60), c) - mstore(0x40, add(matrix, 0x80)) + matrix := relabelled } } @@ -121,82 +148,35 @@ library LibUint256Matrix { /// arrays. Normally `matrix.length` only returns the number of internal /// arrays, not the total number of items in the matrix. /// - /// The running total is guarded against overflow, so a matrix whose sub - /// array length words sum to more than `type(uint256).max` reverts with an - /// arithmetic panic instead of wrapping to a total that is smaller than one - /// of the sub arrays it is totalling. That is only reachable for length - /// words that do not describe real allocations, as every real item costs 32 - /// bytes of memory. + /// Reverts with an arithmetic panic on overflow of the running total. + /// See `LibBytes32Matrix.itemCount`. /// @param matrix The matrix to count the items of. /// @return count The total number of items across every sub array. function itemCount(uint256[][] memory matrix) internal pure returns (uint256 count) { + bytes32[][] memory relabelled; assembly ("memory-safe") { - let cursor := add(matrix, 0x20) - let end := add(cursor, mul(mload(matrix), 0x20)) - - for {} lt(cursor, end) {} { - let subCount := mload(mload(cursor)) - count := add(count, subCount) - // A total that is smaller than the part just added to it - // wrapped. Revert with `Panic(uint256)` code 0x11, byte for - // byte what checked Solidity arithmetic produces for an - // overflow. Memory 0x00 to 0x40 is scratch space, so writing - // the revert data there is memory safe. - if lt(count, subCount) { - mstore(0x00, 0x4e487b71) - mstore(0x20, 0x11) - revert(0x1c, 0x24) - } - cursor := add(cursor, 0x20) - } + relabelled := matrix } + return LibBytes32Matrix.itemCount(relabelled); } /// Allocates and builds a new `uint256[]` from a `uint256[][]`. This is /// potentially memory intensive and expensive, but there's no way around - /// the allocation if a flat array is needed. This is because 2-dimensional - /// arrays are stored as a length-prefixed array of pointers to 1-dimensional - /// arrays, not as a contiguous block of memory. - /// - /// The item count is scaled to a size in bytes with checked arithmetic, so - /// a matrix whose total item count cannot be scaled reverts with an - /// arithmetic panic. A scaling that wrapped would size the allocation from - /// one number while the length word stamped on the result is another, - /// handing back an ordinary `uint256[]` whose declared length far exceeds - /// the memory reserved for it. Solidity's own bounds check trusts that - /// length word, so such an array is a read and write primitive over the - /// rest of memory. + /// the allocation if a flat array is needed. /// - /// The per sub array scaling in the copy loop needs no guard of its own. - /// The checked scaling above bounds the total at `type(uint256).max / - /// 0x20`, and because `itemCount` sums without wrapping, no sub array - /// declares more items than that total, so scaling a sub array length - /// cannot wrap either. The copy loop re-reads those same length words and - /// cannot itself have disturbed them, as it only ever writes above the free - /// memory pointer as it stood on entry and every allocated sub array is - /// below it. + /// Reverts with an arithmetic panic if the total item count cannot be + /// scaled to a size in bytes. See `LibBytes32Matrix.flatten`. /// @param matrix The matrix to flatten. /// @return The flattened array. function flatten(uint256[][] memory matrix) internal pure returns (uint256[] memory) { - uint256 length = itemCount(matrix); - uint256 dataSize = length * 0x20; - + bytes32[][] memory relabelledMatrix; + assembly ("memory-safe") { + relabelledMatrix := matrix + } + bytes32[] memory flattened = LibBytes32Matrix.flatten(relabelledMatrix); uint256[] memory array; assembly ("memory-safe") { - array := mload(0x40) - mstore(0x40, add(array, add(0x20, dataSize))) - mstore(array, length) - - let cursor := add(matrix, 0x20) - let end := add(cursor, mul(mload(matrix), 0x20)) - - let arrayCursor := add(array, 0x20) - for {} lt(cursor, end) {} { - let size := mul(mload(mload(cursor)), 0x20) - mcopy(arrayCursor, add(mload(cursor), 0x20), size) - arrayCursor := add(arrayCursor, size) - cursor := add(cursor, 0x20) - } + array := flattened } return array; }