diff --git a/README.md b/README.md index b374c19..63ca88d 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # rain.sol.codegen + +Solidity native tooling to generate Solidity code. + +Notably builds a valid Solidity file (pragma, etc.) that passes foundry +formatting cleanly, that can build the constant caches needed for prebuilt +function pointer tables to ensure runtime gas efficiency. + +Includes interfaces for the interpreter and sub parsers/externs for Rain +contracts to implement and be compatible with the code generation functions here. + +`script/BuildPointers.sol` includes an example implementation and +`.github/workflows/git-clean.yaml` an example CI action to show how to build +pointers cleanly and ensure that the source code does not become out of sync with +the built artifacts when merging new code. + +Generated code is intended to be imported downstream into contracts that may +themselves expose pointers to be included in the generated code. This circular +dependency means the pointers may need to be built several times until they +produce a stable output where the pointers do not move, and therefore do not +break the codehash over the contract that includes the pointers. diff --git a/flake.lock b/flake.lock index 894a168..9a12e0c 100644 --- a/flake.lock +++ b/flake.lock @@ -75,11 +75,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1758705030, - "narHash": "sha256-zYM8PiEXANNrtjfyGUc7w37/D/kCynp0cQS+wCQ77GI=", + "lastModified": 1769324704, + "narHash": "sha256-aef15vEgiMEls1hTMt46rJuKNSO2cIOfiP99patq9yc=", "owner": "shazow", "repo": "foundry.nix", - "rev": "b59a55014050110170023e3e1c277c1d4a2f055b", + "rev": "e830409ba1bdecdc5ef9a1ec92660fc2da9bc68d", "type": "github" }, "original": { @@ -102,13 +102,29 @@ "type": "indirect" } }, + "nixpkgs-old": { + "locked": { + "lastModified": 1749104371, + "narHash": "sha256-m2NmOPd6XgBiskmUq/BS9Xxuf3z0ebnGVfSKNAO5NEM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "48975d7f9b9960ed33c4e8561bcce20cc0c2de5b", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "48975d7f9b9960ed33c4e8561bcce20cc0c2de5b", + "type": "github" + } + }, "nixpkgs_2": { "locked": { - "lastModified": 1758711836, - "narHash": "sha256-uBqPg7wNX2v6YUdTswH7wWU8wqb60cFZx0tHaWTGF30=", + "lastModified": 1769364508, + "narHash": "sha256-Wy8EVYSLq5Fb/rYH3LRxAMCnW75f9hOg2562AXVFmPk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "46f97b78e825ae762c0224e3983c47687436a498", + "rev": "6077bc4fb29be43d525984f63b69d37b9b1e62fe", "type": "github" }, "original": { @@ -135,11 +151,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1748662220, - "narHash": "sha256-7gGa49iB9nCnFk4h/g9zwjlQAyjtpgcFkODjcOQS0Es=", + "lastModified": 1766653575, + "narHash": "sha256-TPgxCS7+hWc4kPhzkU5dD2M5UuPhLuuaMNZ/IpwKQvI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "59138c7667b7970d205d6a05a8bfa2d78caa3643", + "rev": "3c1016e6acd16ad96053116d0d3043029c9e2649", "type": "github" }, "original": { @@ -154,15 +170,16 @@ "flake-utils": "flake-utils_2", "foundry": "foundry", "nixpkgs": "nixpkgs_2", + "nixpkgs-old": "nixpkgs-old", "rust-overlay": "rust-overlay", "solc": "solc" }, "locked": { - "lastModified": 1760460761, - "narHash": "sha256-IHvwnmphDaOyZnzvObwOoDQlA9nzym2ZUxe9K/5vs0U=", + "lastModified": 1769366341, + "narHash": "sha256-jeYOweTuJdKshW9lqVoNxvl4+flyRzWxEctRGabTW/8=", "owner": "rainprotocol", "repo": "rainix", - "rev": "add0d8a1fd76ce0e65b962c952e9252257876465", + "rev": "e7bfe9c39d2de818eac241f88ecabc69e86ed734", "type": "github" }, "original": { @@ -182,11 +199,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1758681214, - "narHash": "sha256-8cW731vev6kfr58cILO2ZsjHwaPhm88dQ8Q6nTSjP9I=", + "lastModified": 1769309768, + "narHash": "sha256-AbOIlNO+JoqRJkK1VrnDXhxuX6CrdtIu2hSuy4pxi3g=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "b12ed88d8d33d4f3cbc842bf29fad93bb1437299", + "rev": "140c9dc582cb73ada2d63a2180524fcaa744fad5", "type": "github" }, "original": { @@ -202,11 +219,11 @@ "solc-macos-amd64-list-json": "solc-macos-amd64-list-json" }, "locked": { - "lastModified": 1756368702, - "narHash": "sha256-cqEHv7uCV0LibmQphyiXZ1+jYtGjMNb9Pae4tfcAcF8=", + "lastModified": 1768831671, + "narHash": "sha256-0mmlYRtZK+eomevkQCCH7PL8QlSuALZQsjLroCWGE08=", "owner": "hellwolf", "repo": "solc.nix", - "rev": "d83e90df2fa8359a690f6baabf76099432193c3f", + "rev": "80ad871b93d15c7bccf71617f78f73c2d291a9c7", "type": "github" }, "original": { @@ -218,13 +235,13 @@ "solc-macos-amd64-list-json": { "flake": false, "locked": { - "narHash": "sha256-AvITkfpNYgCypXuLJyqco0li+unVw39BAfdOZvd/SPE=", + "narHash": "sha256-P+ZslplK4cQ/wnV/wykVKb+yTCviI0eylA3sk9uHmRo=", "type": "file", - "url": "https://github.com/argotorg/solc-bin/raw/26fc3fd/macosx-amd64/list.json" + "url": "https://github.com/argotorg/solc-bin/raw/a11f1ad/macosx-amd64/list.json" }, "original": { "type": "file", - "url": "https://github.com/argotorg/solc-bin/raw/26fc3fd/macosx-amd64/list.json" + "url": "https://github.com/argotorg/solc-bin/raw/a11f1ad/macosx-amd64/list.json" } }, "systems": { diff --git a/foundry.lock b/foundry.lock index 743b4b3..b5439a3 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,8 +1,8 @@ { "lib/forge-std": { - "rev": "b8f065fda83b8cd94a6b2fec8fcd911dc3b444fd" + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" }, "lib/rain.math.binary": { - "rev": "f44f846a43928ba0a7ed4a7bb810a8933e23cce1" + "rev": "122a490bb1869c7533f108cb8b371d75de9db60f" } } \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index b8f065f..1801b05 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit b8f065fda83b8cd94a6b2fec8fcd911dc3b444fd +Subproject commit 1801b0541f4fda118a10798fd3486bb7051c5dd6 diff --git a/lib/rain.math.binary b/lib/rain.math.binary index f44f846..122a490 160000 --- a/lib/rain.math.binary +++ b/lib/rain.math.binary @@ -1 +1 @@ -Subproject commit f44f846a43928ba0a7ed4a7bb810a8933e23cce1 +Subproject commit 122a490bb1869c7533f108cb8b371d75de9db60f diff --git a/script/BuildPointers.sol b/script/BuildPointers.sol index 1539571..01db584 100644 --- a/script/BuildPointers.sol +++ b/script/BuildPointers.sol @@ -7,7 +7,13 @@ import {LibFs} from "../src/lib/LibFs.sol"; import {LibCodeGen} from "../src/lib/LibCodeGen.sol"; import {CodeGennable} from "../test/concrete/CodeGennable.sol"; +/// @title BuildPointers +/// @notice Script to build the pointers file for the CodeGennable contract. +/// @dev This shows an example of how to use the bytes constant generation +/// utility in LibCodeGen. contract BuildPointers is Script { + /// Builds the pointers file for the CodeGennable contract to show an example + /// of how to use the bytes constant generation utility. function run() external { CodeGennable codeGennable = new CodeGennable(); @@ -16,13 +22,15 @@ contract BuildPointers is Script { address(codeGennable), "CodeGennable", string.concat( - LibCodeGen.bytesConstantString(vm, "/// @dev Some bytes comment.", "SOME_BYTES_CONSTANT", hex"12345678"), LibCodeGen.bytesConstantString( - vm, - "/// @dev Longer constant.", - "LONGER_BYTES_CONSTANT", - hex"e2bafcba65b2c99d33f5096307bc57c2e7f195d2a178f56e45d720bb64344998e2bafcba65b2c99d33f5096307bc57c2e7f195d2a178f56e45d720bb64344998" - ) + vm, "/// @dev Some bytes comment.", "SOME_BYTES_CONSTANT", hex"12345678" + ), + LibCodeGen.bytesConstantString( + vm, + "/// @dev Longer constant.", + "LONGER_BYTES_CONSTANT", + hex"e2bafcba65b2c99d33f5096307bc57c2e7f195d2a178f56e45d720bb64344998e2bafcba65b2c99d33f5096307bc57c2e7f195d2a178f56e45d720bb64344998" + ) ) ); } diff --git a/src/interface/IIntegrityToolingV1.sol b/src/interface/IIntegrityToolingV1.sol index cec5148..a4c6ef9 100644 --- a/src/interface/IIntegrityToolingV1.sol +++ b/src/interface/IIntegrityToolingV1.sol @@ -2,6 +2,19 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; +/// @title IIntegrityToolingV1 +/// Implemented by any contract that exposes integrity check functions for the +/// Rain interpreter. Ostensibly this is for the interpreter itself and also +/// extension points such as externs. These integrity checks are for the opcodes +/// tooled by IOpcodeToolingV1 implementations. interface IIntegrityToolingV1 { + /// Builds integrity function pointers. + /// This is intended for use by the Rain interpreter to run integrity checks + /// over rainlang code before it is used/deployed/executed. The expectation + /// is that the pointers will be built ahead of time and cached in a constant + /// for efficiency. As the process is deterministic for a given source and + /// compiler configuration, the output can be tested against the used value + /// in CI and the translation from source to pointers can also be tested in + /// CI. See .github/workflows/git-clean.yaml for an example of such a test. function buildIntegrityFunctionPointers() external view returns (bytes memory); } diff --git a/src/interface/IOpcodeToolingV1.sol b/src/interface/IOpcodeToolingV1.sol index 228de71..459e3e8 100644 --- a/src/interface/IOpcodeToolingV1.sol +++ b/src/interface/IOpcodeToolingV1.sol @@ -2,6 +2,18 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; +/// @title IOpcodeToolingV1 +/// Implemented by any contract that exposes opcode functions (new words) for the +/// Rain interpreter. Ostensibly this is for the interpreter itself and also +/// extension points such as externs. interface IOpcodeToolingV1 { + /// Builds opcode function pointers. + /// This is intended for use by the Rain interpreter to run opcodes + /// implementing rainlang execution logic. The expectation is that the + /// pointers will be built ahead of time and cached in a constant for + /// efficiency. As the process is deterministic for a given source and + /// compiler configuration, the output can be tested against the used value + /// in CI and the translation from source to pointers can also be tested in + /// CI. See .github/workflows/git-clean.yaml for an example of such a test. function buildOpcodeFunctionPointers() external view returns (bytes memory); } diff --git a/src/interface/IParserToolingV1.sol b/src/interface/IParserToolingV1.sol index edc0260..3d9c3fa 100644 --- a/src/interface/IParserToolingV1.sol +++ b/src/interface/IParserToolingV1.sol @@ -2,8 +2,28 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; +/// @title IParserToolingV1 +/// Implemented by any contract that exposes parser tooling functions for the +/// Rain interpreter. Ostensibly this is for the interpreter itself while sub +/// parsers have a separate interface. interface IParserToolingV1 { + /// Builds operand handler function pointers. + /// This is intended for use by the Rain interpreter to run operand handling + /// logic when parsing rainlang code. The expectation is that the pointers + /// will be built ahead of time and cached in a constant for efficiency. As + /// the process is deterministic for a given source and compiler + /// configuration, the output can be tested against the used value in CI and + /// the translation from source to pointers can also be tested in CI. See + /// .github/workflows/git-clean.yaml for an example of such a test. function buildOperandHandlerFunctionPointers() external pure returns (bytes memory); + /// Builds literal parser function pointers. + /// This is intended for use by the Rain interpreter to run literal parsing + /// logic when parsing rainlang code. The expectation is that the pointers + /// will be built ahead of time and cached in a constant for efficiency. As + /// the process is deterministic for a given source and compiler + /// configuration, the output can be tested against the used value in CI and + /// the translation from source to pointers can also be tested in CI. See + /// .github/workflows/git-clean.yaml for an example of such a test. function buildLiteralParserFunctionPointers() external pure returns (bytes memory); } diff --git a/src/interface/ISubParserToolingV1.sol b/src/interface/ISubParserToolingV1.sol index 2d86764..1fd1c16 100644 --- a/src/interface/ISubParserToolingV1.sol +++ b/src/interface/ISubParserToolingV1.sol @@ -2,6 +2,17 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity ^0.8.25; +/// @title ISubParserToolingV1 +/// Implemented by any contract that exposes sub parser tooling functions for the +/// Rain interpreter. interface ISubParserToolingV1 { + /// Builds sub parser word parsers. + /// This is intended for use by the Rain interpreter to run sub parser word + /// parsing logic when parsing rainlang code. The expectation is that the + /// parsers will be built ahead of time and cached in a constant for + /// efficiency. As the process is deterministic for a given source and + /// compiler configuration, the output can be tested against the used value + /// in CI and the translation from source to parsers can also be tested in + /// CI. See .github/workflows/git-clean.yaml for an example of such a test. function buildSubParserWordParsers() external pure returns (bytes memory); } diff --git a/src/lib/LibCodeGen.sol b/src/lib/LibCodeGen.sol index 452c668..0fa7cc0 100644 --- a/src/lib/LibCodeGen.sol +++ b/src/lib/LibCodeGen.sol @@ -9,11 +9,26 @@ import {ISubParserToolingV1} from "../interface/ISubParserToolingV1.sol"; import {IIntegrityToolingV1} from "../interface/IIntegrityToolingV1.sol"; import {LibHexString} from "./LibHexString.sol"; +/// @dev Maximum length of a line in the generated code. Important limit for +/// compatibility with formatters such as `foundry fmt`. uint256 constant MAX_LINE_LENGTH = 120; + +/// @dev Newline string used when a line exceeds the max length. Indentation +/// needs to match what formatters expect. string constant NEWLINE_DUE_TO_MAX_LENGTH = "\n "; +/// @title LibCodeGen +/// @notice Library for generating Solidity code snippets for contract function +/// pointers, code hashes, associated comments, etc. All snippets are returned +/// as strings that can be concatenated into complete Solidity files and written +/// to disk by the caller. library LibCodeGen { + /// The file prefix for autogenerated files outlines the license, pragma, + /// and a note about the file being autogenerated. The pragma is ^ as the + /// generated code is expected to be imported into some concrete contract + /// with pragma = version. function filePrefix() internal pure returns (string memory) { + //REUSE-IgnoreStart return string.concat( "// SPDX-License-Identifier: LicenseRef-DCL-1.0\n" "// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd\n" @@ -24,8 +39,17 @@ library LibCodeGen { "// needs the pointers file to exist so that it can compile, and the pointers\n" "// file needs the contract to exist so that it can be compiled.\n" ); + //REUSE-IgnoreEnd } + /// Puts the hash of the bytecode of some contract instance into a constant + /// string. Often used to ensure that the deployed bytecode matches the + /// expected bytecode. + /// @param vm The Vm instance for file operations. + /// @param instance The address of the contract instance whose bytecode + /// hash is to be computed. + /// @return A string containing the Solidity code for the bytecode hash + /// constant. function bytecodeHashConstantString(Vm vm, address instance) internal view returns (string memory) { bytes32 bytecodeHash; assembly { @@ -40,6 +64,13 @@ library LibCodeGen { ); } + /// Puts the opcode function pointers used by the interpreter into a + /// constant string. + /// @param vm The Vm instance for file operations. + /// @param interpreter The interpreter tooling instance to get the + /// function pointers from. + /// @return A string containing the Solidity code for the opcode function + /// pointers constant. function opcodeFunctionPointersConstantString(Vm vm, IOpcodeToolingV1 interpreter) internal view @@ -58,6 +89,13 @@ library LibCodeGen { ); } + /// Puts the literal parser function pointers used by the parser into a + /// constant string. + /// @param vm The Vm instance for file operations. + /// @param instance The parser tooling instance to get the function pointers + /// from. + /// @return A string containing the Solidity code for the literal parser + /// function pointers constant. function literalParserFunctionPointersConstantString(Vm vm, IParserToolingV1 instance) internal pure @@ -77,6 +115,13 @@ library LibCodeGen { ); } + /// Puts the operand handler function pointers used by the parser into a + /// constant string. + /// @param vm The Vm instance for file operations. + /// @param instance The parser tooling instance to get the function pointers + /// from. + /// @return A string containing the Solidity code for the operand handler + /// function pointers constant. function operandHandlerFunctionPointersConstantString(Vm vm, IParserToolingV1 instance) internal pure @@ -94,6 +139,13 @@ library LibCodeGen { ); } + /// Puts the sub parser word parser function pointers used by the sub parser + /// into a constant string. + /// @param vm The Vm instance for file operations. + /// @param subParser The sub parser tooling instance to get the function + /// pointers from. + /// @return A string containing the Solidity code for the sub parser word + /// parsers constant. function subParserWordParsersConstantString(Vm vm, ISubParserToolingV1 subParser) internal pure @@ -113,6 +165,13 @@ library LibCodeGen { ); } + /// Puts the integrity check function pointers used by the integrity tooling + /// into a constant string. + /// @param vm The Vm instance for file operations. + /// @param deployer The integrity tooling instance to get the function + /// pointers from. + /// @return A string containing the Solidity code for the integrity check + /// function pointers constant. function integrityFunctionPointersConstantString(Vm vm, IIntegrityToolingV1 deployer) internal view @@ -126,6 +185,12 @@ library LibCodeGen { ); } + /// Puts the hash of the meta that describes the contract into a constant + /// string. + /// @param vm The Vm instance for file operations. + /// @param name The name of the contract whose meta hash is to be computed. + /// @return A string containing the Solidity code for the described by meta + /// hash constant. function describedByMetaHashConstantString(Vm vm, string memory name) internal view returns (string memory) { bytes memory describedByMeta = vm.readFileBinary(string.concat("meta/", name, ".rain.meta")); return string.concat( @@ -137,6 +202,14 @@ library LibCodeGen { ); } + /// Generates a Solidity bytes constant declaration string. Needs special + /// handling to be formatted nicely due to potential length of hex data and + /// constant name. + /// @param vm The Vm instance for file operations. + /// @param comment The comment to include above the constant declaration. + /// @param name The name of the constant. + /// @param data The bytes data for the constant. + /// @return A string containing the Solidity code for the bytes constant. function bytesConstantString(Vm vm, string memory comment, string memory name, bytes memory data) internal pure @@ -156,6 +229,13 @@ library LibCodeGen { ); } + /// Generates a Solidity uint8 constant declaration string. Needs special + /// handling to be formatted nicely due to potential length of constant name. + /// @param vm The Vm instance for file operations. + /// @param comment The comment to include above the constant declaration. + /// @param name The name of the constant. + /// @param data The uint8 data for the constant. + /// @return A string containing the Solidity code for the uint8 constant. function uint8ConstantString(Vm vm, string memory comment, string memory name, uint8 data) internal pure diff --git a/src/lib/LibFs.sol b/src/lib/LibFs.sol index bf7e275..778d3c0 100644 --- a/src/lib/LibFs.sol +++ b/src/lib/LibFs.sol @@ -5,14 +5,32 @@ pragma solidity ^0.8.25; import {Vm} from "forge-std/Vm.sol"; import {LibCodeGen} from "./LibCodeGen.sol"; +/// @title LibFs +/// @notice A library for file system operations related to code generation. +/// @dev Uses foundry's Vm cheat codes for file operations. Notably standardizes +/// the placement and idempotent creation of generated files. library LibFs { + /// @notice Constructs the file path for a generated contract's pointers + /// file. + /// @param contractName The name of the contract. + /// @return The file path as a string. function pathForContract(string memory contractName) internal pure returns (string memory) { return string.concat("src/generated/", contractName, ".pointers.sol"); } + /// @notice Builds a file for a generated contract, removing any existing + /// file at the same path. This ensures idempotent file generation but will + /// delete any manual changes to the generated file, or existing file at + /// that path. The prefix and bytecode hash constant are always included, + /// further content is provided in the body parameter, which is expected to + /// be generated by `LibCodeGen` by the caller. + /// @param vm The Vm instance for file operations. + /// @param instance The contract instance whose bytecode hash is to be + /// included. + /// @param contractName The name of the contract. + /// @param body The body of the contract file to be written. function buildFileForContract(Vm vm, address instance, string memory contractName, string memory body) internal { string memory path = pathForContract(contractName); - if (vm.exists(path)) { //forge-lint: disable-next-line(unsafe-cheatcode) vm.removeFile(path); diff --git a/src/lib/LibHexString.sol b/src/lib/LibHexString.sol index a6070b1..b3b7b25 100644 --- a/src/lib/LibHexString.sol +++ b/src/lib/LibHexString.sol @@ -4,11 +4,21 @@ pragma solidity ^0.8.25; import {Vm} from "forge-std/Vm.sol"; +/// @title LibHexString +/// @notice A library for converting bytes to hexadecimal strings. Uses the +/// standard foundry Vm to perform the conversion. library LibHexString { + /// Converts a bytes array to its hexadecimal string representation but + /// without the leading "0x". This is useful because solidity does not always + /// accept the prefix, such as in `hex"..."` literals. + /// @param vm The Vm instance used for conversion. + /// @param data The bytes array to convert. + /// @return The hexadecimal string representation of the bytes array. function bytesToHex(Vm vm, bytes memory data) internal pure returns (string memory) { string memory hexString = vm.toString(data); assembly ("memory-safe") { - // Remove the leading 0x + // Remove the leading 0x which is unconditionally added by + /// vm.toString. let newHexString := add(hexString, 2) mstore(newHexString, sub(mload(hexString), 2)) hexString := newHexString diff --git a/test/concrete/CodeGennable.sol b/test/concrete/CodeGennable.sol index 7770fab..a627c09 100644 --- a/test/concrete/CodeGennable.sol +++ b/test/concrete/CodeGennable.sol @@ -2,4 +2,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; +/// @title CodeGennable +/// An empty contract used by `script/BuildPointers.sol` as a way to show an +/// example of the code generation capabilities of this repo. contract CodeGennable {}