diff --git a/test/concrete/ToolingMock.sol b/test/concrete/ToolingMock.sol new file mode 100644 index 0000000..ca42233 --- /dev/null +++ b/test/concrete/ToolingMock.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +/// @title ToolingMock +/// Answers all five tooling builders that `LibCodeGen` dispatches to, each with +/// its own byte string, so a test can tell which builder a `LibCodeGen` wrapper +/// actually called rather than only that it called something. +/// @dev `IParserToolingV1` and `ISubParserToolingV1` declare their builders +/// `pure`, which no implementation can satisfy while returning data set per +/// instance, so this mock declares all five `view` and does not inherit the +/// interfaces. Tests cast its address to the interface at the call site, so the +/// selectors are still the interfaces' own. +contract ToolingMock { + bytes internal sOpcodeFunctionPointers; + bytes internal sLiteralParserFunctionPointers; + bytes internal sOperandHandlerFunctionPointers; + bytes internal sSubParserWordParsers; + bytes internal sIntegrityFunctionPointers; + + /// Sets every builder's answer at once. Tests pass five distinct values so + /// that a wrapper calling the wrong builder emits the wrong hex. + function setAll( + bytes memory opcodeFunctionPointers, + bytes memory literalParserFunctionPointers, + bytes memory operandHandlerFunctionPointers, + bytes memory subParserWordParsers, + bytes memory integrityFunctionPointers + ) external { + sOpcodeFunctionPointers = opcodeFunctionPointers; + sLiteralParserFunctionPointers = literalParserFunctionPointers; + sOperandHandlerFunctionPointers = operandHandlerFunctionPointers; + sSubParserWordParsers = subParserWordParsers; + sIntegrityFunctionPointers = integrityFunctionPointers; + } + + function buildOpcodeFunctionPointers() external view returns (bytes memory) { + return sOpcodeFunctionPointers; + } + + function buildLiteralParserFunctionPointers() external view returns (bytes memory) { + return sLiteralParserFunctionPointers; + } + + function buildOperandHandlerFunctionPointers() external view returns (bytes memory) { + return sOperandHandlerFunctionPointers; + } + + function buildSubParserWordParsers() external view returns (bytes memory) { + return sSubParserWordParsers; + } + + function buildIntegrityFunctionPointers() external view returns (bytes memory) { + return sIntegrityFunctionPointers; + } +} diff --git a/test/lib/LibCodeGen.addressConstantString.t.sol b/test/lib/LibCodeGen.addressConstantString.t.sol index 138ff1f..65016e2 100644 --- a/test/lib/LibCodeGen.addressConstantString.t.sol +++ b/test/lib/LibCodeGen.addressConstantString.t.sol @@ -3,7 +3,13 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev A checksummed address literal, 42 characters like every other, so the +/// declaration is `72 + name.length` characters long on one line. +address constant SOME_ADDRESS = address(0xc51a14251b0dcF0ae24A96b7153991378938f5F5); +string constant SOME_ADDRESS_STRING = "0xc51a14251b0dcF0ae24A96b7153991378938f5F5"; /// @title LibCodeGenAddressConstantStringTest /// @notice `addressConstantString` emits a Solidity `address constant` @@ -36,4 +42,43 @@ contract LibCodeGenAddressConstantStringTest is Test { ); assertEq(vm.parseAddress(vm.toString(data)), data); } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testAddressConstantStringAtMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(48); + string memory emitted = LibCodeGen.addressConstantString(vm, "/// @dev At max.", name, SOME_ADDRESS); + assertEq( + emitted, + string.concat("\n/// @dev At max.\naddress constant ", name, " = address(", SOME_ADDRESS_STRING, ");\n") + ); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testAddressConstantStringOverMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(49); + assertEq( + LibCodeGen.addressConstantString(vm, "/// @dev Over max.", name, SOME_ADDRESS), + string.concat( + "\n/// @dev Over max.\naddress constant ", name, " =\n address(", SOME_ADDRESS_STRING, ");\n" + ) + ); + } + + /// Whatever the comment, name and address, the emitted text is the + /// declaration built from those literals, wrapped exactly when measuring the + /// one line form says it does not fit. Fuzzed over every input because each + /// term of the library's hand computed sum has to be right for this to hold. + function testAddressConstantStringMatchesMeasuredLine(string memory comment, string memory name, address data) + external + view + { + assertEq( + LibCodeGen.addressConstantString(vm, comment, name, data), + LibCodeGenSlow.addressConstantStringSlow(vm, comment, name, data) + ); + } } diff --git a/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol new file mode 100644 index 0000000..9177377 --- /dev/null +++ b/test/lib/LibCodeGen.bytecodeHashConstantString.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {CodeGennable} from "../concrete/CodeGennable.sol"; + +/// @title LibCodeGenBytecodeHashConstantStringTest +/// @notice `bytecodeHashConstantString` emits the one constant that every +/// generated file carries, under a name and comment it chooses itself. What it +/// must emit is the hash of the runtime code actually at the instance address, +/// because a consumer compares it against `addr.codehash`, so these derive the +/// expectation by hashing that code rather than by reading it back off the same +/// account. +contract LibCodeGenBytecodeHashConstantStringTest is Test { + address internal constant INSTANCE = address(uint160(uint256(keccak256("instance")))); + + /// `vm.etch` refuses code shaped like an EIP-7702 delegation designator — + /// leading bytes `0xef01` — unless it is exactly the 23 bytes such a + /// designator has to be. Measured against the cheatcode rather than assumed: + /// `0xef01` + 21 bytes is accepted, `0xef01` at 22, 24 and 2 bytes is + /// refused, and `0xef`, `0xef0000` and `0xef02…` are all accepted, so the + /// refusal is exactly this two byte prefix at a length other than 23. + /// + /// That is a restriction on what the cheatcode will install at an address, + /// not a property of `bytecodeHashConstantString`, so the fuzzer's domain + /// excludes it. Without this the suite passes or fails on the luck of the + /// fuzz seed: the seeds CI happened to draw never reached `0xef01…`, and + /// seeds drawn locally did, failing on the first run with + /// `vm.etch: failed to create bytecode: Eip7702 is not 23 bytes long`. + function assumeEtchableCode(bytes memory code) internal pure { + vm.assume(code.length > 0); + vm.assume(!(code.length >= 2 && code[0] == 0xef && code[1] == 0x01 && code.length != 23)); + } + + /// The whole emitted declaration, for code put at the address by hand. The + /// name and comment are the library's, not the caller's, so they are pinned + /// exactly: every consumer's generated file and every consumer's assertion + /// against it is written against this spelling. + function testBytecodeHashConstantString() external { + vm.etch(INSTANCE, hex"6001"); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(hex"6001")), + ");\n" + ) + ); + } + + /// The hash is of the runtime code of a really deployed contract, which is + /// what `addr.codehash` returns for it. + function testBytecodeHashConstantStringDeployed() external { + CodeGennable codeGennable = new CodeGennable(); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, address(codeGennable)), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(address(codeGennable).code)), + ");\n" + ) + ); + } + + /// The name is fixed and so is the length of a `bytes32` literal, so this + /// declaration can never need wrapping. Asserted rather than assumed, + /// because this function does its own concatenation instead of going through + /// `bytes32ConstantString` and so has no wrap decision at all. + function testBytecodeHashConstantStringFitsMaxLength() external { + vm.etch(INSTANCE, hex"6001"); + assertLe(LibCodeGenSlow.longestLineSlow(LibCodeGen.bytecodeHashConstantString(vm, INSTANCE)), MAX_LINE_LENGTH); + } + + /// Whatever the runtime code, the constant carries its keccak256 hash. + function testBytecodeHashConstantStringHashesCode(bytes memory code) external { + assumeEtchableCode(code); + vm.etch(INSTANCE, code); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), + string.concat( + "\n/// @dev Hash of the known bytecode.\nbytes32 constant BYTECODE_HASH = bytes32(", + vm.toString(keccak256(code)), + ");\n" + ) + ); + } + + /// Two instances with different code get different constants, so the + /// constant is a fingerprint of the code rather than of the address or of + /// anything else about the account. + function testBytecodeHashConstantStringDiscriminatesCode(bytes memory codeA, bytes memory codeB) external { + assumeEtchableCode(codeA); + assumeEtchableCode(codeB); + vm.assume(keccak256(codeA) != keccak256(codeB)); + + vm.etch(INSTANCE, codeA); + string memory emittedA = LibCodeGen.bytecodeHashConstantString(vm, INSTANCE); + vm.etch(INSTANCE, codeB); + string memory emittedB = LibCodeGen.bytecodeHashConstantString(vm, INSTANCE); + + assertNotEq(emittedA, emittedB); + } + + /// The same instance generates the same text every time it is asked, so + /// regenerating a file twice does not produce a diff. + function testBytecodeHashConstantStringIdempotent(bytes memory code) external { + assumeEtchableCode(code); + vm.etch(INSTANCE, code); + assertEq( + LibCodeGen.bytecodeHashConstantString(vm, INSTANCE), LibCodeGen.bytecodeHashConstantString(vm, INSTANCE) + ); + } +} diff --git a/test/lib/LibCodeGen.bytes32ConstantString.t.sol b/test/lib/LibCodeGen.bytes32ConstantString.t.sol index 4d4e72d..484a0d1 100644 --- a/test/lib/LibCodeGen.bytes32ConstantString.t.sol +++ b/test/lib/LibCodeGen.bytes32ConstantString.t.sol @@ -3,7 +3,13 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev A `bytes32` literal is 66 characters whatever the value, so the +/// declaration is `96 + name.length` characters long on one line. +bytes32 constant SOME_HASH = 0x2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f; +string constant SOME_HASH_STRING = "0x2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; /// @title LibCodeGenBytes32ConstantStringTest /// @notice `bytes32ConstantString` emits a Solidity `bytes32 constant` @@ -43,4 +49,41 @@ contract LibCodeGenBytes32ConstantStringTest is Test { ); assertEq(vm.parseBytes32(vm.toString(data)), data); } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testBytes32ConstantStringAtMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(24); + string memory emitted = LibCodeGen.bytes32ConstantString(vm, "/// @dev At max.", name, SOME_HASH); + assertEq( + emitted, + string.concat("\n/// @dev At max.\nbytes32 constant ", name, " = bytes32(", SOME_HASH_STRING, ");\n") + ); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testBytes32ConstantStringOverMaxLength() external view { + string memory name = LibCodeGenSlow.nameOfLengthSlow(25); + assertEq( + LibCodeGen.bytes32ConstantString(vm, "/// @dev Over max.", name, SOME_HASH), + string.concat("\n/// @dev Over max.\nbytes32 constant ", name, " =\n bytes32(", SOME_HASH_STRING, ");\n") + ); + } + + /// Whatever the comment, name and value, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. Fuzzed over every input because each term of + /// the library's hand computed sum has to be right for this to hold. + function testBytes32ConstantStringMatchesMeasuredLine(string memory comment, string memory name, bytes32 data) + external + view + { + assertEq( + LibCodeGen.bytes32ConstantString(vm, comment, name, data), + LibCodeGenSlow.bytes32ConstantStringSlow(vm, comment, name, data) + ); + } } diff --git a/test/lib/LibCodeGen.bytesConstantString.t.sol b/test/lib/LibCodeGen.bytesConstantString.t.sol new file mode 100644 index 0000000..7809082 --- /dev/null +++ b/test/lib/LibCodeGen.bytesConstantString.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev 32 bytes, so the hex literal is 64 characters and the whole declaration +/// is `24 + name.length + 64` characters on one line. +bytes constant DATA_32 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; +string constant HEX_32 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420f"; + +/// @dev One byte more than `DATA_32`, so two characters more of hex. +bytes constant DATA_33 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faa"; +string constant HEX_33 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faa"; + +/// @dev Two bytes more than `DATA_32`, so four characters more of hex. +bytes constant DATA_34 = hex"2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faabb"; +string constant HEX_34 = "2573004ac3a9ee7fc8d73654d76386f1b6b99e34cdf86a689c4691e47143420faabb"; + +/// @title LibCodeGenBytesConstantStringTest +/// @notice `bytesConstantString` emits a Solidity `bytes constant` declaration +/// and decides for itself whether that declaration fits on one line. The +/// decision is made by adding up magic numbers standing in for the literals it +/// is about to concatenate, so these assert the emitted text against a reference +/// that measures the line instead, and pin the decision either side of the +/// maximum. +contract LibCodeGenBytesConstantStringTest is Test { + /// The short case: blank line, comment, then the whole declaration on one + /// line. The blank line is what separates this constant from whatever the + /// caller concatenated before it. + function testBytesConstantString() external pure { + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Some bytes comment.", "SOME_BYTES_CONSTANT", hex"12345678"), + "\n/// @dev Some bytes comment.\nbytes constant SOME_BYTES_CONSTANT = hex\"12345678\";\n" + ); + } + + /// Empty data is a real value, not a reason to skip the constant. `hex""` is + /// valid Solidity for empty bytes, so the declaration is still emitted whole + /// rather than collapsing to a bare `hex` or an unterminated literal. + function testBytesConstantStringEmptyData() external pure { + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Nothing.", "NOTHING", ""), + "\n/// @dev Nothing.\nbytes constant NOTHING = hex\"\";\n" + ); + } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testBytesConstantStringAtMaxLength() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(32); + string memory emitted = LibCodeGen.bytesConstantString(vm, "/// @dev At max.", name, DATA_32); + assertEq(emitted, string.concat("\n/// @dev At max.\nbytes constant ", name, " = hex\"", HEX_32, "\";\n")); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testBytesConstantStringOverMaxLengthByName() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(33); + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Over max.", name, DATA_32), + string.concat("\n/// @dev Over max.\nbytes constant ", name, " =\n hex\"", HEX_32, "\";\n") + ); + } + + /// The data's own length counts toward the decision, not just the name's. + /// Same name either side, one byte of data apart. + function testBytesConstantStringOverMaxLengthByData() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(30); + + string memory under = LibCodeGen.bytesConstantString(vm, "/// @dev Under.", name, DATA_33); + assertEq(under, string.concat("\n/// @dev Under.\nbytes constant ", name, " = hex\"", HEX_33, "\";\n")); + assertEq(LibCodeGenSlow.longestLineSlow(under), MAX_LINE_LENGTH); + + assertEq( + LibCodeGen.bytesConstantString(vm, "/// @dev Over.", name, DATA_34), + string.concat("\n/// @dev Over.\nbytes constant ", name, " =\n hex\"", HEX_34, "\";\n") + ); + } + + /// Whatever the comment, name and data, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. Fuzzed over every input because each term of + /// the library's hand computed sum has to be right for this to hold. + function testBytesConstantStringMatchesMeasuredLine(string memory comment, string memory name, bytes memory data) + external + pure + { + assertEq( + LibCodeGen.bytesConstantString(vm, comment, name, data), + LibCodeGenSlow.bytesConstantStringSlow(vm, comment, name, data) + ); + } + + /// The hex the declaration carries is the data, unchanged and unprefixed, so + /// the constant compiles back to the bytes it was generated from. A `0x` + /// inside a `hex"..."` literal does not compile at all. + function testBytesConstantStringCarriesData(bytes memory data) external pure { + string memory emitted = LibCodeGen.bytesConstantString(vm, "/// @dev Fuzz.", "FUZZ", data); + assertTrue(vm.contains(emitted, string.concat("hex\"", LibCodeGenSlow.hexOfSlow(vm, data), "\";\n"))); + assertFalse(vm.contains(emitted, "hex\"0x"), "hex literal carries a 0x prefix"); + } +} diff --git a/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol new file mode 100644 index 0000000..f284779 --- /dev/null +++ b/test/lib/LibCodeGen.describedByMetaHashConstantString.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @dev `describedByMetaHashConstantString` reads `meta/.rain.meta`, and +/// this repo's `fs_permissions` grants no read under `meta`. `src/generated` is +/// the one directory it grants read-write, so a fixture goes there and the name +/// walks back out of `meta` to reach it. That the name reaches the path at all +/// is itself asserted below. +/// +/// The fixture file is real, shared, mutable state that outlives the EVM: every +/// test here writes it and then removes it. A single path shared across the +/// tests is therefore a race — one test's `removeFile` lands between another's +/// `writeFileBinary` and the library's read, and that test fails with +/// `vm.readFileBinary: ... No such file or directory` on a file it had just +/// written. It reproduced as an intermittent 2-of-5 failure in this suite at a +/// fixed `--fuzz-seed`, so the fuzzer was never involved. Each test owns a +/// distinct path instead, which is what makes them independent of each other. +string constant FIXTURE_STEM = "LibCodeGenDescribedByMetaHashFixture"; + +/// @dev The fixed part of the declaration. The name and comment are the +/// library's own choice rather than the caller's, so both are pinned exactly: +/// every consumer's generated file is written against this spelling. +string constant DESCRIBED_BY_META_HASH_PREFIX = + "\n/// @dev The hash of the meta that describes the contract.\nbytes32 constant DESCRIBED_BY_META_HASH = bytes32("; + +/// @title LibCodeGenDescribedByMetaHashConstantStringTest +/// @notice `describedByMetaHashConstantString` builds a path out of the name it +/// is given, reads that file, and puts the hash of its contents into a constant. +contract LibCodeGenDescribedByMetaHashConstantStringTest is Test { + /// Reachable only through an external call so that a refused read can be + /// caught and inspected rather than aborting the test. + function callDescribedByMetaHash(string memory name) external view returns (string memory) { + return LibCodeGen.describedByMetaHashConstantString(vm, name); + } + + /// The path this test's own fixture is written to, relative to the repo root. + function fixturePath(string memory owner) internal pure returns (string memory) { + return string.concat("src/generated/", FIXTURE_STEM, owner, ".rain.meta"); + } + + /// The name that makes the library build `fixturePath(owner)`: it walks back + /// out of the `meta/` directory the library prepends, and stops short of the + /// `.rain.meta` the library appends. + function fixtureName(string memory owner) internal pure returns (string memory) { + return string.concat("../src/generated/", FIXTURE_STEM, owner); + } + + /// The path is `meta/.rain.meta`. The name is not a file that exists + /// here, so the path is observed through the access refusal, which quotes + /// the path that was asked for. Two names, because a hard coded path would + /// satisfy one of them. + function testDescribedByMetaHashConstantStringPath() external { + assertRequestsPath("CodeGennable", "meta/CodeGennable.rain.meta"); + assertRequestsPath("SomeOtherContract", "meta/SomeOtherContract.rain.meta"); + } + + function assertRequestsPath(string memory name, string memory expectedPath) internal { + try this.callDescribedByMetaHash(name) returns (string memory) { + fail(); + } catch (bytes memory err) { + assertTrue(vm.contains(string(err), expectedPath), "did not ask for the expected path"); + } + } + + /// The constant carries the keccak256 of the file's bytes, and nothing about + /// the file's name or path. + function testDescribedByMetaHashConstantString() external { + bytes memory meta = hex"1234"; + vm.writeFileBinary(fixturePath("Basic"), meta); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Basic")); + vm.removeFile(fixturePath("Basic")); + + assertEq(emitted, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(meta)), ");\n")); + } + + /// Different meta gives a different constant, so the hash is of the contents + /// rather than of anything fixed. + function testDescribedByMetaHashConstantStringHashesContents() external { + bytes memory metaA = hex"1234"; + bytes memory metaB = hex"5678"; + + vm.writeFileBinary(fixturePath("HashesContents"), metaA); + string memory emittedA = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("HashesContents")); + vm.writeFileBinary(fixturePath("HashesContents"), metaB); + string memory emittedB = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("HashesContents")); + vm.removeFile(fixturePath("HashesContents")); + + assertEq(emittedA, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaA)), ");\n")); + assertEq(emittedB, string.concat(DESCRIBED_BY_META_HASH_PREFIX, vm.toString(keccak256(metaB)), ");\n")); + assertNotEq(emittedA, emittedB); + } + + /// Reading the same file twice gives the same text, so regenerating a file + /// does not produce a diff. + function testDescribedByMetaHashConstantStringIdempotent() external { + vm.writeFileBinary(fixturePath("Idempotent"), hex"1234"); + string memory first = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Idempotent")); + string memory second = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("Idempotent")); + vm.removeFile(fixturePath("Idempotent")); + + assertEq(first, second); + } + + /// The name is fixed and so is the length of a `bytes32` literal, so this + /// declaration can never need wrapping. Asserted rather than assumed, + /// because this function does its own concatenation instead of going through + /// `bytes32ConstantString` and so has no wrap decision at all. + function testDescribedByMetaHashConstantStringFitsMaxLength() external { + vm.writeFileBinary(fixturePath("FitsMaxLength"), hex"1234"); + string memory emitted = LibCodeGen.describedByMetaHashConstantString(vm, fixtureName("FitsMaxLength")); + vm.removeFile(fixturePath("FitsMaxLength")); + + assertLe(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } +} diff --git a/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..4e14431 --- /dev/null +++ b/test/lib/LibCodeGen.integrityFunctionPointersConstantString.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IIntegrityToolingV1} from "src/interface/IIntegrityToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. This is the +/// only one of the five that is a single line, so it is also the only one whose +/// comment is passed as a plain literal rather than a concatenation. +string constant INTEGRITY_COMMENT = "/// @dev The function pointers for the integrity check fns."; + +/// @title LibCodeGenIntegrityFunctionPointersConstantStringTest +/// @notice `integrityFunctionPointersConstantString` names the constant, writes +/// the comment and picks which of the tooling instance's builders to ask. All +/// three are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenIntegrityFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testIntegrityFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"cccc", hex"dddd", hex"1234"); + assertEq( + LibCodeGen.integrityFunctionPointersConstantString(vm, IIntegrityToolingV1(address(sMock))), + "\n/// @dev The function pointers for the integrity check fns.\n" + "bytes constant INTEGRITY_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildIntegrityFunctionPointers` and from no other + /// builder on the same instance. + function testIntegrityFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, other, other, other, pointers); + assertEq( + LibCodeGen.integrityFunctionPointersConstantString(vm, IIntegrityToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow(vm, INTEGRITY_COMMENT, "INTEGRITY_FUNCTION_POINTERS", pointers) + ); + } +} diff --git a/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..872e5e7 --- /dev/null +++ b/test/lib/LibCodeGen.literalParserFunctionPointersConstantString.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IParserToolingV1} from "src/interface/IParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant LITERAL_PARSER_COMMENT = "/// @dev Every two bytes is a function pointer for a literal parser.\n" + "/// Literal dispatches are determined by the first byte(s) of the literal\n" + "/// rather than a full word lookup, and are done with simple conditional\n" + "/// jumps as the possibilities are limited compared to the number of words we\n" "/// have."; + +/// @title LibCodeGenLiteralParserFunctionPointersConstantStringTest +/// @notice `literalParserFunctionPointersConstantString` names the constant, +/// writes the comment and picks which of the tooling instance's builders to ask. +/// All three are the library's own choice rather than the caller's, so all three +/// are pinned here. +contract LibCodeGenLiteralParserFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testLiteralParserFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"1234", hex"bbbb", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.literalParserFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + "\n/// @dev Every two bytes is a function pointer for a literal parser.\n" + "/// Literal dispatches are determined by the first byte(s) of the literal\n" + "/// rather than a full word lookup, and are done with simple conditional\n" + "/// jumps as the possibilities are limited compared to the number of words we\n" "/// have.\n" + "bytes constant LITERAL_PARSER_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildLiteralParserFunctionPointers` and from no + /// other builder on the same instance. `IParserToolingV1` carries two + /// builders, so the sibling on the same interface is the one most easily + /// asked by mistake. + function testLiteralParserFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, pointers, other, other, other); + assertEq( + LibCodeGen.literalParserFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, LITERAL_PARSER_COMMENT, "LITERAL_PARSER_FUNCTION_POINTERS", pointers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..e99c5a2 --- /dev/null +++ b/test/lib/LibCodeGen.opcodeFunctionPointersConstantString.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IOpcodeToolingV1} from "src/interface/IOpcodeToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant OPCODE_COMMENT = "/// @dev The function pointers known to the interpreter for dynamic dispatch.\n" + "/// By setting these as a constant they can be inlined into the interpreter\n" + "/// and loaded at eval time for very low gas (~100) due to the compiler\n" + "/// optimising it to a single `codecopy` to build the in memory bytes array."; + +/// @title LibCodeGenOpcodeFunctionPointersConstantStringTest +/// @notice `opcodeFunctionPointersConstantString` names the constant, writes the +/// comment and picks which of the tooling instance's builders to ask. All three +/// are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenOpcodeFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testOpcodeFunctionPointersConstantString() external { + sMock.setAll(hex"1234", hex"aaaa", hex"bbbb", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.opcodeFunctionPointersConstantString(vm, IOpcodeToolingV1(address(sMock))), + "\n/// @dev The function pointers known to the interpreter for dynamic dispatch.\n" + "/// By setting these as a constant they can be inlined into the interpreter\n" + "/// and loaded at eval time for very low gas (~100) due to the compiler\n" + "/// optimising it to a single `codecopy` to build the in memory bytes array.\n" + "bytes constant OPCODE_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildOpcodeFunctionPointers` and from no other + /// builder on the same instance. The mock answers each builder differently, + /// so asking the wrong one emits the wrong hex. + function testOpcodeFunctionPointersConstantStringUsesItsOwnBuilder(bytes memory pointers, bytes memory other) + external + { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(pointers, other, other, other, other); + assertEq( + LibCodeGen.opcodeFunctionPointersConstantString(vm, IOpcodeToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow(vm, OPCODE_COMMENT, "OPCODE_FUNCTION_POINTERS", pointers) + ); + } +} diff --git a/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol b/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol new file mode 100644 index 0000000..53e437f --- /dev/null +++ b/test/lib/LibCodeGen.operandHandlerFunctionPointersConstantString.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {IParserToolingV1} from "src/interface/IParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant OPERAND_HANDLER_COMMENT = "/// @dev Every two bytes is a function pointer for an operand handler.\n" + "/// These positional indexes all map to the same indexes looked up in the parse\n" "/// meta."; + +/// @title LibCodeGenOperandHandlerFunctionPointersConstantStringTest +/// @notice `operandHandlerFunctionPointersConstantString` names the constant, +/// writes the comment and picks which of the tooling instance's builders to ask. +/// All three are the library's own choice rather than the caller's, so all three +/// are pinned here. +contract LibCodeGenOperandHandlerFunctionPointersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short pointer string. + function testOperandHandlerFunctionPointersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"1234", hex"cccc", hex"dddd"); + assertEq( + LibCodeGen.operandHandlerFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + "\n/// @dev Every two bytes is a function pointer for an operand handler.\n" + "/// These positional indexes all map to the same indexes looked up in the parse\n" "/// meta.\n" + "bytes constant OPERAND_HANDLER_FUNCTION_POINTERS = hex\"1234\";\n" + ); + } + + /// The pointers come from `buildOperandHandlerFunctionPointers` and from no + /// other builder on the same instance. `IParserToolingV1` carries two + /// builders, so the sibling on the same interface is the one most easily + /// asked by mistake. + function testOperandHandlerFunctionPointersConstantStringUsesItsOwnBuilder( + bytes memory pointers, + bytes memory other + ) external { + vm.assume(keccak256(pointers) != keccak256(other)); + sMock.setAll(other, other, pointers, other, other); + assertEq( + LibCodeGen.operandHandlerFunctionPointersConstantString(vm, IParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, OPERAND_HANDLER_COMMENT, "OPERAND_HANDLER_FUNCTION_POINTERS", pointers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol b/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol new file mode 100644 index 0000000..73f4af8 --- /dev/null +++ b/test/lib/LibCodeGen.subParserWordParsersConstantString.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen} from "src/lib/LibCodeGen.sol"; +import {ISubParserToolingV1} from "src/interface/ISubParserToolingV1.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; +import {ToolingMock} from "../concrete/ToolingMock.sol"; + +/// @dev The comment the library puts above this constant, spelled out here so +/// that a change to it fails rather than moving both sides at once. +string constant SUB_PARSER_WORD_PARSERS_COMMENT = "/// @dev The function pointers for the sub parser functions that produce the\n" + "/// bytecode that this contract knows about. This is both constructing the subParser\n" + "/// bytecode that dials back into this contract at eval time, and mapping\n" + "/// to things that happen entirely on the interpreter such as well known\n" + "/// constants and references to the context grid."; + +/// @title LibCodeGenSubParserWordParsersConstantStringTest +/// @notice `subParserWordParsersConstantString` names the constant, writes the +/// comment and picks which of the tooling instance's builders to ask. All three +/// are the library's own choice rather than the caller's, so all three are +/// pinned here. +contract LibCodeGenSubParserWordParsersConstantStringTest is Test { + ToolingMock internal sMock; + + function setUp() external { + sMock = new ToolingMock(); + } + + /// The whole emitted declaration for a short parser string. + function testSubParserWordParsersConstantString() external { + sMock.setAll(hex"aaaa", hex"bbbb", hex"cccc", hex"1234", hex"dddd"); + assertEq( + LibCodeGen.subParserWordParsersConstantString(vm, ISubParserToolingV1(address(sMock))), + "\n/// @dev The function pointers for the sub parser functions that produce the\n" + "/// bytecode that this contract knows about. This is both constructing the subParser\n" + "/// bytecode that dials back into this contract at eval time, and mapping\n" + "/// to things that happen entirely on the interpreter such as well known\n" + "/// constants and references to the context grid.\n" + "bytes constant SUB_PARSER_WORD_PARSERS = hex\"1234\";\n" + ); + } + + /// The parsers come from `buildSubParserWordParsers` and from no other + /// builder on the same instance. + function testSubParserWordParsersConstantStringUsesItsOwnBuilder(bytes memory parsers, bytes memory other) + external + { + vm.assume(keccak256(parsers) != keccak256(other)); + sMock.setAll(other, other, other, parsers, other); + assertEq( + LibCodeGen.subParserWordParsersConstantString(vm, ISubParserToolingV1(address(sMock))), + LibCodeGenSlow.bytesConstantStringSlow( + vm, SUB_PARSER_WORD_PARSERS_COMMENT, "SUB_PARSER_WORD_PARSERS", parsers + ) + ); + } +} diff --git a/test/lib/LibCodeGen.uint8ConstantString.t.sol b/test/lib/LibCodeGen.uint8ConstantString.t.sol new file mode 100644 index 0000000..7bebae5 --- /dev/null +++ b/test/lib/LibCodeGen.uint8ConstantString.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibCodeGen, MAX_LINE_LENGTH} from "src/lib/LibCodeGen.sol"; +import {LibCodeGenSlow} from "./LibCodeGenSlow.sol"; + +/// @title LibCodeGenUint8ConstantStringTest +/// @notice `uint8ConstantString` emits a Solidity `uint8 constant` declaration +/// and decides for itself whether that declaration fits on one line. The +/// decision is made by adding up magic numbers standing in for the literals it +/// is about to concatenate, so these assert the emitted text against a reference +/// that measures the line instead, and pin the decision either side of the +/// maximum. +contract LibCodeGenUint8ConstantStringTest is Test { + /// The short case: blank line, comment, then the whole declaration on one + /// line, decimal rather than hex. + function testUint8ConstantString() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Some count.", "SOME_COUNT", 42), + "\n/// @dev Some count.\nuint8 constant SOME_COUNT = 42;\n" + ); + } + + /// Zero is a real value, not a sentinel to special-case. + function testUint8ConstantStringZero() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Zero.", "ZERO", 0), + "\n/// @dev Zero.\nuint8 constant ZERO = 0;\n" + ); + } + + /// The maximum a `uint8` holds still emits as a plain decimal that fits the + /// declared type, so the generated constant compiles. + function testUint8ConstantStringMax() external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Max.", "MAX", type(uint8).max), + "\n/// @dev Max.\nuint8 constant MAX = 255;\n" + ); + } + + /// A declaration of exactly the maximum length stays on one line. `forge fmt` + /// leaves a line of exactly `line_length` alone, so wrapping here would be a + /// reflow the formatter immediately undoes. + function testUint8ConstantStringAtMaxLength() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(98); + string memory emitted = LibCodeGen.uint8ConstantString(vm, "/// @dev At max.", name, 255); + assertEq(emitted, string.concat("\n/// @dev At max.\nuint8 constant ", name, " = 255;\n")); + assertEq(LibCodeGenSlow.longestLineSlow(emitted), MAX_LINE_LENGTH); + } + + /// One character past the maximum wraps after the `=`, with the value + /// indented by one tab width on the next line. + function testUint8ConstantStringOverMaxLengthByName() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(99); + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Over max.", name, 255), + string.concat("\n/// @dev Over max.\nuint8 constant ", name, " =\n 255;\n") + ); + } + + /// The decimal's own width counts toward the decision, not just the name's. + /// Same name either side, one digit apart. + function testUint8ConstantStringOverMaxLengthByDigits() external pure { + string memory name = LibCodeGenSlow.nameOfLengthSlow(100); + + string memory under = LibCodeGen.uint8ConstantString(vm, "/// @dev Under.", name, 9); + assertEq(under, string.concat("\n/// @dev Under.\nuint8 constant ", name, " = 9;\n")); + assertEq(LibCodeGenSlow.longestLineSlow(under), MAX_LINE_LENGTH); + + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Over.", name, 10), + string.concat("\n/// @dev Over.\nuint8 constant ", name, " =\n 10;\n") + ); + } + + /// Whatever the comment, name and value, the emitted text is the declaration + /// built from those literals, wrapped exactly when measuring the one line + /// form says it does not fit. + function testUint8ConstantStringMatchesMeasuredLine(string memory comment, string memory name, uint8 data) + external + pure + { + assertEq( + LibCodeGen.uint8ConstantString(vm, comment, name, data), + LibCodeGenSlow.uint8ConstantStringSlow(vm, comment, name, data) + ); + } + + /// The emitted literal parses back to the value it was generated from, so + /// the constant is not silently truncated or reformatted. + function testUint8ConstantStringRoundTrips(uint8 data) external pure { + assertEq( + LibCodeGen.uint8ConstantString(vm, "/// @dev Fuzz.", "FUZZ", data), + string.concat("\n/// @dev Fuzz.\nuint8 constant FUZZ = ", vm.toString(uint256(data)), ";\n") + ); + assertEq(vm.parseUint(vm.toString(uint256(data))), uint256(data)); + } +} diff --git a/test/lib/LibCodeGenSlow.sol b/test/lib/LibCodeGenSlow.sol new file mode 100644 index 0000000..05c2f2b --- /dev/null +++ b/test/lib/LibCodeGenSlow.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Vm} from "forge-std-1.16.1/src/Vm.sol"; + +/// @dev `forge fmt`'s own `line_length` default. Written out here rather than +/// imported from `LibCodeGen` so that this reference does not move when the +/// library's constant does. +uint256 constant SLOW_LINE_LENGTH = 120; + +/// @dev `forge fmt` breaks a too long constant declaration after the `=` and +/// indents the value by one `tab_width`, which also defaults to 4. +string constant SLOW_WRAP = "\n "; + +/// @title LibCodeGenSlow +/// @notice A deliberately naive reference for the constant declarations +/// `LibCodeGen` emits. +/// +/// `LibCodeGen` decides whether to wrap by adding up magic numbers that stand in +/// for the literals it is about to concatenate. This reference instead builds +/// the unwrapped line and measures it, so the two agree only when every one of +/// those magic numbers is right. Every string here is spelled out again rather +/// than imported, so a change to a literal in `LibCodeGen` shows up as a +/// disagreement instead of moving both sides at once. +library LibCodeGenSlow { + /// `vm.toString` on a `bytes` always prefixes `0x`, which a `hex"..."` + /// literal must not carry. Dropped by copying the tail one byte at a time so + /// the expectation owes nothing to `LibHexString`. + function hexOfSlow(Vm vm, bytes memory data) internal pure returns (string memory) { + bytes memory prefixed = bytes(vm.toString(data)); + bytes memory stripped = new bytes(prefixed.length - 2); + for (uint256 i = 2; i < prefixed.length; i++) { + stripped[i - 2] = prefixed[i]; + } + return string(stripped); + } + + /// Joins the declaration onto one line when it fits, and onto two when it + /// does not. `declaration` is everything up to and including the `=`, and + /// `value` is everything after it. + function joinSlow(string memory declaration, string memory value) internal pure returns (string memory) { + string memory oneLine = string.concat(declaration, " ", value); + if (bytes(oneLine).length > SLOW_LINE_LENGTH) { + return string.concat(declaration, SLOW_WRAP, value); + } + return oneLine; + } + + function bytesConstantStringSlow(Vm vm, string memory comment, string memory name, bytes memory data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow(string.concat("bytes constant ", name, " ="), string.concat("hex\"", hexOfSlow(vm, data), "\";")), + "\n" + ); + } + + function uint8ConstantStringSlow(Vm vm, string memory comment, string memory name, uint8 data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow(string.concat("uint8 constant ", name, " ="), string.concat(vm.toString(uint256(data)), ";")), + "\n" + ); + } + + function bytes32ConstantStringSlow(Vm vm, string memory comment, string memory name, bytes32 data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow( + string.concat("bytes32 constant ", name, " ="), string.concat("bytes32(", vm.toString(data), ");") + ), + "\n" + ); + } + + function addressConstantStringSlow(Vm vm, string memory comment, string memory name, address data) + internal + pure + returns (string memory) + { + return string.concat( + "\n", + comment, + "\n", + joinSlow( + string.concat("address constant ", name, " ="), string.concat("address(", vm.toString(data), ");") + ), + "\n" + ); + } + + /// The length of the longest line in `text`, so a test can assert what + /// `forge fmt` would measure rather than what the library predicted. + function longestLineSlow(string memory text) internal pure returns (uint256) { + bytes memory data = bytes(text); + uint256 longest = 0; + uint256 current = 0; + for (uint256 i = 0; i < data.length; i++) { + if (data[i] == 0x0a) { + if (current > longest) { + longest = current; + } + current = 0; + } else { + current++; + } + } + if (current > longest) { + longest = current; + } + return longest; + } + + /// A name of `length` repeated `A` characters, for pinning the wrap decision + /// either side of the maximum line length without spelling out a name that + /// nobody can count by eye. + function nameOfLengthSlow(uint256 length) internal pure returns (string memory) { + bytes memory name = new bytes(length); + for (uint256 i = 0; i < length; i++) { + name[i] = "A"; + } + return string(name); + } +}