├── .gitignore ├── .gitmodules ├── foundry.toml ├── LICENSE ├── .github └── workflows │ └── test.yml ├── README.md ├── src └── Generators.sol └── test └── Generators.t.sol /.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | out/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | branch = v1 3 | path = lib/forge-std 4 | url = https://github.com/foundry-rs/forge-std 5 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # ================================= 2 | # ======== Default Profile ======== 3 | # ================================= 4 | [profile.default] 5 | verbosity = 3 6 | 7 | # ============================ 8 | # ======== CI Profile ======== 9 | # ============================ 10 | [profile.ci.fuzz] 11 | runs = 10000 12 | 13 | # ============================ 14 | # ======== Formatting ======== 15 | # ============================ 16 | [profile.default.fmt] 17 | line_length = 80 18 | tab_width = 2 19 | bracket_spacing = false 20 | int_types = 'short' 21 | multiline_func_header = 'attributes_first' 22 | quote_style = 'double' 23 | number_underscore = 'thousands' 24 | single_line_statement_blocks = 'single' 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Matt Solomon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | env: 11 | FOUNDRY_PROFILE: ci 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Install Foundry 20 | uses: foundry-rs/foundry-toolchain@v1 21 | with: 22 | version: nightly 23 | 24 | - name: Install deps 25 | run: forge install 26 | 27 | - name: Build and check sizes 28 | run: | 29 | forge --version 30 | forge build --sizes 31 | 32 | test: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Install Foundry 38 | uses: foundry-rs/foundry-toolchain@v1 39 | with: 40 | version: nightly 41 | 42 | - name: Install deps 43 | run: forge install 44 | 45 | - name: Run tests 46 | run: forge test 47 | 48 | fmt: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v3 52 | 53 | - name: Install Foundry 54 | uses: foundry-rs/foundry-toolchain@v1 55 | with: 56 | version: nightly 57 | 58 | - name: forge fmt 59 | run: forge fmt --check -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Array Generators 2 | 3 | Solidity library offering `linspace`, `arange`, and `logspace` methods to generate evenly spaced arrays. 4 | Both signed and unsigned integers are supported. 5 | 6 | This library was written for use in development/testing: 7 | 8 | - It is not designed to be gas efficient. 9 | - It has not been heavily tested and may contain bugs. 10 | 11 | Also, it's not a library in the Solidity sense, it's really just a file full of [free functions](https://docs.soliditylang.org/en/latest/contracts.html#functions). 12 | 13 | ## Installation 14 | 15 | To use this in a [Foundry](https://github.com/foundry-rs/foundry) project, install it with: 16 | 17 | ```sh 18 | forge install https://github.com/mds1/solidity-generators 19 | ``` 20 | 21 | There is no npm package, so for projects using npm for package management, such as [Hardhat](https://hardhat.org/) projects, use: 22 | 23 | ```sh 24 | yarn add https://github.com/mds1/solidity-generators.git 25 | ``` 26 | 27 | ## Usage 28 | 29 | This library generates evenly spaced arrays between the specified start and stop values, inclusive. 30 | Function names and signatures are similar to those of [numpy](https://numpy.org/), and this library currently supports: 31 | 32 | - `linspace`: Returns linearly spaced numbers over a specified interval, specifying the length of the array. Defaults to 50 elements in the array if not specified. 33 | - `arange`: Returns linearly spaced numbers over a specified interval, specifying the step size. Defaults to a step size of 1 if not specified. 34 | - `logspace`: Returns numbers spaced evenly on a log scale. Unlike `linspace` and `arange`, you don't enter the start and stop values directly, but their exponents. Start and stop values are then calculated as `base ** start` and `base ** stop`. Defaults to 50 elements in the array and a base of 10 if not specified. 35 | 36 | Each method behaves as follows: 37 | 38 | - If start is smaller than stop, the array will be in ascending order. 39 | - If start is larger than stop, the array will be in descending order. 40 | - If the start and stop values are the same, the array will contain a single element of that value. 41 | 42 | Arrays default to a length of 50 and step size of 1 unless specified otherwise. 43 | 44 | Although the start and stop values are inclusive, the stop value is not guaranteed to be in the array depending on the specified parameters. 45 | See the examples below for more info. 46 | 47 | Note that current implementation of `logspace` is naive, and therefore will revert if the parameters passed are not valid `linspace` args. 48 | If there is demand for it, a future version may remove this restriction. 49 | 50 | ## Examples 51 | 52 | ```solidity 53 | import {linspace, arange, logspace} from "solidity-generators/Generators.sol"; 54 | 55 | uint256 ustart = 0; 56 | uint256 ustop = 1000; 57 | 58 | int256 istart = -1000; 59 | int256 istop = 1000; 60 | 61 | // ========================== 62 | // ======== Linspace ======== 63 | // ========================== 64 | // Linear spacing, by number of elements. 65 | 66 | linspace(ustart, ustop); 67 | // Returns [0, 20, 40, ..., 940, 960, 980] 68 | 69 | linspace(ustart, ustop, 50); 70 | // Returns [0, 20, 40, ..., 940, 960, 980] 71 | 72 | linspace(ustart, ustop, 51); 73 | // Returns [0, 20, 40, ..., 940, 960, 980, 1000] 74 | 75 | 76 | linspace(istart, istop); 77 | // Returns [-1000, -960, -920, ..., 880, 920, 960] 78 | 79 | linspace(istart, istop, 50); 80 | // Returns [-1000, -960, -920, ..., 880, 920, 960] 81 | 82 | linspace(istart, istop, 51); 83 | // Returns [-1000, -960, -920, ..., 880, 920, 960, 1000] 84 | 85 | // ======================== 86 | // ======== Arange ======== 87 | // ======================== 88 | // Linear spacing, by step size. 89 | 90 | arange(ustart, ustop); 91 | // Returns [0, 1, 2, ..., 998, 999, 1000] 92 | 93 | arange(ustart, ustop, 250); 94 | // Returns [0, 250, 500, 750, 1000] 95 | 96 | 97 | arange(istart, istop, 500); 98 | // Returns [-1000, -500, 0, 500, 1000] 99 | 100 | arange(istart, istop); 101 | // Returns [-1000, -999, -998, ..., 998, 999, 1000] 102 | 103 | // ========================== 104 | // ======== Logspace ======== 105 | // ========================== 106 | // Logarithmic Spacing. 107 | 108 | logspace(0, 6, 7); // Base 10. 109 | // Returns [1, 10, 100, 1000, 10_000, 100_000, 1_000_000] 110 | 111 | logspace(2, 10, 9, 2); // Base 2. 112 | // Returns [4, 8, 16, 32, 64, 128, 256, 512, 1024] 113 | 114 | // ================================ 115 | // ======== Other Examples ======== 116 | // ================================ 117 | 118 | // We can flip the order for a descending array. 119 | arange(istop, istart); 120 | // Returns [1000, 999, 998, ..., -998, -999, -1000] 121 | 122 | logspace(6, 0, 7); // Base 10. 123 | // Returns [1_000_000, 100_000, 10_000, 1000, 100, 10, 1] 124 | 125 | 126 | // Bounds are not guaranteed to be exclusive if not cleanly divisible. 127 | linspace(ustart, 10, 4); 128 | // Returns [0, 3, 6, 9] 129 | 130 | arange(ustart, 10, 3); 131 | // Returns [0, 3, 6, 9] 132 | ``` 133 | -------------------------------------------------------------------------------- /src/Generators.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /** 5 | * @dev Library to generate evenly spaced arrays between the specified start and 6 | * stop values, inclusive. If start is greater than stop, the array will be in 7 | * ascending order. If stop is greater than start, the array will be in 8 | * descending order. If the start and stop values are the same, the array will 9 | * contain a single element of that value. 10 | * 11 | * Generating the array by specifying number of elements is the typical API 12 | * used in other languages, so this is the default. However, there are also 13 | * methods that let you specify the step size between elements instead. 14 | * 15 | * Although the start and stop values are inclusive, the stop value is not 16 | * guaranteed to be in the array depending on the specified parameters. For 17 | * example, `linspace(1, 10, 3)` will return `[1, 5, 9]`. 18 | * 19 | * Both signed and unsigned integers are supported for linearly spaced arrays. 20 | * Logarithmically spaced arrays are not yet supported. 21 | */ 22 | 23 | // ========================== 24 | // ======== Linspace ======== 25 | // ========================== 26 | // Linear spacing, by number of elements. 27 | 28 | function linspace(uint start, uint stop) pure returns (uint[] memory arr) { 29 | arr = linspace(start, stop, 50); 30 | } 31 | 32 | function linspace(int start, int stop) pure returns (int[] memory arr) { 33 | arr = linspace(start, stop, 50); 34 | } 35 | 36 | function linspace(uint start, uint stop, uint num) 37 | pure 38 | returns (uint[] memory arr) 39 | { 40 | // Flip start and stop if generating a descending array. 41 | bool desc = start > stop; 42 | if (desc) (start, stop) = (stop, start); 43 | 44 | // Compute step size. 45 | uint size = stop - start; 46 | if (size == 0) num = 1; 47 | require(num - 1 <= size, "num larger than range"); // Prevent step size of 0 when num > 1 48 | uint step = num == 1 ? 0 : size / (num - 1); 49 | 50 | // Generate array. 51 | arr = new uint256[](num); 52 | for (uint i = 0; i < num; i++) { 53 | arr[i] = desc ? stop - i * step : start + i * step; 54 | } 55 | } 56 | 57 | function linspace(int start, int stop, uint num) 58 | pure 59 | returns (int[] memory arr) 60 | { 61 | // Flip start and stop if generating a descending array. 62 | bool desc = start > stop; 63 | if (desc) (start, stop) = (stop, start); 64 | 65 | // Compute step size. 66 | uint size = range(start, stop); 67 | if (size == 0) num = 1; 68 | require(num - 1 <= size, "num larger than range"); // Prevent step size of 0 when num > 1. 69 | uint step = num == 1 ? 0 : size / (num - 1); 70 | 71 | // Generate array. 72 | arr = new int256[](num); 73 | for (uint i = 0; i < num; i++) { 74 | // Unlike the unsigned case, the signed case requires that we add `step` 75 | // to the prior array value instead of simply using `start + i * step`. 76 | // This is because `i * step` may be larger than type(int256).max for 77 | // ranges such as `linspace(type(int256).min, 1, 100)`, and we can't 78 | // represent that gap as a uint256 and add the uint256 to an int256. 79 | if (i == 0) arr[0] = desc ? stop : start; 80 | else arr[i] = desc ? arr[i - 1] - int(step) : arr[i - 1] + int(step); 81 | } 82 | } 83 | 84 | // ======================== 85 | // ======== Arange ======== 86 | // ======================== 87 | // Linear spacing, by step size. 88 | 89 | // Default to a step size of 1. 90 | function arange(uint start, uint stop) pure returns (uint[] memory arr) { 91 | arr = arange(start, stop, 1); 92 | } 93 | 94 | function arange(int start, int stop) pure returns (int[] memory arr) { 95 | arr = arange(start, stop, 1); 96 | } 97 | 98 | // Specify the step size. 99 | function arange(uint start, uint stop, uint step) 100 | pure 101 | returns (uint[] memory arr) 102 | { 103 | uint num = (range(start, stop) / step) + 1; 104 | arr = linspace(start, stop, num); 105 | } 106 | 107 | function arange(int start, int stop, uint step) pure returns (int[] memory arr) { 108 | uint num = (range(start, stop) / step) + 1; 109 | arr = linspace(start, stop, num); 110 | } 111 | 112 | // ========================== 113 | // ======== Logspace ======== 114 | // ========================== 115 | // Logarithmic Spacing. 116 | 117 | // Default to 100 elements. 118 | function logspace(uint start, uint stop) pure returns (uint[] memory arr) { 119 | arr = logspace(start, stop, 50); 120 | } 121 | 122 | // Specify the number of elements. 123 | function logspace(uint start, uint stop, uint num) 124 | pure 125 | returns (uint[] memory arr) 126 | { 127 | arr = logspace(start, stop, num, 10); 128 | } 129 | 130 | // Specify the number of elements and the exponent base. 131 | function logspace(uint start, uint stop, uint num, uint base) 132 | pure 133 | returns (uint[] memory arr) 134 | { 135 | // TODO Improve robustness so `linspace(2, 3, 4, 10)` returns `[100, 215, 464, 1000]` instead of reverting. 136 | arr = linspace(start, stop, num); 137 | for (uint i = 0; i < arr.length; i++) { 138 | arr[i] = base ** arr[i]; 139 | } 140 | } 141 | 142 | // ======================= 143 | // ======== Utils ======== 144 | // ======================= 145 | 146 | function range(uint a, uint b) pure returns (uint c) { 147 | c = a > b ? a - b : b - a; 148 | } 149 | 150 | function range(int a, int b) pure returns (uint c) { 151 | bool sameSign = a >= 0 && b >= 0 || a < 0 && b < 0; 152 | c = sameSign ? range(abs(a), abs(b)) : abs(a) + abs(b); 153 | } 154 | 155 | function abs(int a) pure returns (uint b) { 156 | // Unchecked is required to handle the case when n = type(int256).min 157 | unchecked { 158 | b = uint(a >= 0 ? a : -a); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/Generators.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | import {linspace, arange, logspace, range} from "../src/Generators.sol"; 6 | 7 | // ======================= 8 | // ======== Setup ======== 9 | // ======================= 10 | contract GeneratorsTest is Test { 11 | // Test vectors. 12 | uint[] unsignedExpectedAscending = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 13 | uint[] unsignedExpectedDescending = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; 14 | 15 | int[] signedExpectedAscending = [int(-5), -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]; 16 | int[] signedExpectedDescending = [int(5), 4, 3, 2, 1, 0, -1, -2, -3, -4, -5]; 17 | 18 | int[] signedExpectedAscending2 = [int(0), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 19 | int[] signedExpectedDescending2 = [int(10), 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; 20 | 21 | uint[] logspaceAscending = [1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6]; 22 | uint[] logspaceDescending = [1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1]; 23 | 24 | uint[] logspaceAscending2 = [4, 8, 16, 32, 64, 128, 256, 512, 1024]; 25 | uint[] logspaceDescending2 = [1024, 512, 256, 128, 64, 32, 16, 8, 4]; 26 | } 27 | 28 | // ========================== 29 | // ======== Linspace ======== 30 | // ========================== 31 | 32 | contract LinspaceUnsigned is GeneratorsTest { 33 | function test_Ascending() public { 34 | uint[] memory array = linspace(uint(0), 10, 11); 35 | assertEq(array, unsignedExpectedAscending); 36 | } 37 | 38 | function test_Descending() public { 39 | uint[] memory array = linspace(uint(10), 0, 11); 40 | assertEq(array, unsignedExpectedDescending); 41 | } 42 | 43 | function test_StartEqualsStop() public { 44 | uint[] memory array = linspace(uint(25), 25); 45 | assertEq(array.length, 1); 46 | assertEq(array[0], 25); 47 | } 48 | 49 | function testFuzz_Overloads(uint start, uint stop) public { 50 | // Bound minimum difference to avoid reverting from num - 1 <= size. 51 | vm.assume(range(start, stop) >= 49); // Using 49 since default size is 50. 52 | 53 | uint[] memory a = linspace(start, stop); 54 | uint[] memory b = linspace(start, stop, 50); 55 | assertEq(a, b); 56 | } 57 | } 58 | 59 | contract LinspaceSigned is GeneratorsTest { 60 | function test_Ascending() public { 61 | int[] memory array = linspace(-5, 5, 11); 62 | assertEq(array, signedExpectedAscending); 63 | 64 | int[] memory array2 = linspace(int(0), 10, 11); 65 | assertEq(array2, signedExpectedAscending2); 66 | } 67 | 68 | function test_Descending() public { 69 | int[] memory array = linspace(5, -5, 11); 70 | assertEq(array, signedExpectedDescending); 71 | 72 | int[] memory array2 = linspace(int(10), 0, 11); 73 | assertEq(array2, signedExpectedDescending2); 74 | } 75 | 76 | function test_StartEqualsStop() public { 77 | int[] memory array = linspace(int(25), 25); 78 | assertEq(array.length, 1); 79 | assertEq(array[0], 25); 80 | 81 | int[] memory array2 = linspace(-20, -20); 82 | assertEq(array2.length, 1); 83 | assertEq(array2[0], -20); 84 | } 85 | 86 | function testFuzz_Overloads(int start, int stop) public { 87 | // Bound minimum difference to avoid reverting from num - 1 <= size. 88 | vm.assume(range(start, stop) >= 49); // Using 49 since default size is 50. 89 | 90 | int[] memory a = linspace(start, stop); 91 | int[] memory b = linspace(start, stop, 50); 92 | assertEq(a, b); 93 | } 94 | } 95 | 96 | // ======================= 97 | // ======== Arange ======== 98 | // ======================= 99 | 100 | contract ArangeUnsigned is GeneratorsTest { 101 | function test_Ascending() public { 102 | uint[] memory array = arange(uint(0), 10, 1); 103 | assertEq(array, unsignedExpectedAscending); 104 | } 105 | 106 | function test_Descending() public { 107 | uint[] memory array = arange(uint(10), 0, 1); 108 | assertEq(array, unsignedExpectedDescending); 109 | } 110 | 111 | function test_StartEqualsStop() public { 112 | uint[] memory array = arange(uint(25), 25); 113 | assertEq(array.length, 1); 114 | assertEq(array[0], 25); 115 | } 116 | 117 | function testFuzz_Overloads(uint start, uint stop) public { 118 | // Bound max difference to avoid unrealistic/long-running test cases. 119 | if (stop > start && stop - start > 1000) start = stop - 1000; 120 | if (stop < start && start - stop > 1000) stop = start - 1000; 121 | 122 | uint[] memory a = arange(start, stop); 123 | uint[] memory b = arange(start, stop, 1); 124 | assertEq(a, b); 125 | } 126 | } 127 | 128 | contract ArangeSigned is GeneratorsTest { 129 | function test_Ascending() public { 130 | int[] memory array = arange(-5, 5, 1); 131 | assertEq(array, signedExpectedAscending); 132 | } 133 | 134 | function test_Descending() public { 135 | int[] memory array = arange(5, -5, 1); 136 | assertEq(array, signedExpectedDescending); 137 | } 138 | 139 | function test_StartEqualsStop() public { 140 | int[] memory array = arange(int(25), 25); 141 | assertEq(array.length, 1); 142 | assertEq(array[0], 25); 143 | 144 | int[] memory array2 = arange(-20, -20); 145 | assertEq(array2.length, 1); 146 | assertEq(array2[0], -20); 147 | } 148 | 149 | function testFuzz_Overloads(int start, int stop) public { 150 | // Bound max difference to avoid unrealistic/long-running test cases. 151 | if (stop > start && range(start, stop) > 1000) start = stop - 1000; 152 | if (stop < start && range(start, stop) > 1000) stop = start - 1000; 153 | 154 | int[] memory a = arange(start, stop); 155 | int[] memory b = arange(start, stop, 1); 156 | assertEq(a, b); 157 | } 158 | } 159 | 160 | // ========================== 161 | // ======== Logspace ======== 162 | // ========================== 163 | 164 | contract Logspace is GeneratorsTest { 165 | function test_Ascending() public { 166 | uint[] memory array = logspace(0, 6, 7); // Base 10. 167 | assertEq(array, logspaceAscending); 168 | 169 | uint[] memory array2 = logspace(2, 10, 10 - 2 + 1, 2); // Base 2. 170 | assertEq(array2, logspaceAscending2); 171 | } 172 | 173 | function test_Descending() public { 174 | uint[] memory array = logspace(6, 0, 7); // Base 10. 175 | assertEq(array, logspaceDescending); 176 | 177 | uint[] memory array2 = logspace(10, 2, 10 - 2 + 1, 2); // Base 2. 178 | assertEq(array2, logspaceDescending2); 179 | } 180 | 181 | function testFuzz_Overloads(uint start, uint stop) public { 182 | // Bound numbers to prevent overflow. 183 | start = bound(start, 0, 10); 184 | stop = bound(stop, start + 49, start + 49 + 20); // Ensure we don't revert due to zero step size. 185 | 186 | uint[] memory a = logspace(start, stop); 187 | uint[] memory b = logspace(start, stop, 50, 10); 188 | assertEq(a, b); 189 | } 190 | } 191 | --------------------------------------------------------------------------------