Skip to content

Commit

Permalink
Merge pull request #5759 from NomicFoundation/refactor/streamline-con…
Browse files Browse the repository at this point in the history
…sole-library-generator

refactor: Streamline `console.sol` generation
  • Loading branch information
Xanewok authored Sep 19, 2024
2 parents 74b1bfe + 5b410f9 commit f93c447
Show file tree
Hide file tree
Showing 4 changed files with 754 additions and 760 deletions.
2 changes: 1 addition & 1 deletion packages/hardhat-core/console.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ library console {
function log() internal pure {
_sendLogPayload(abi.encodeWithSignature("log()"));
}

function logInt(int256 p0) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(int256)", p0));
}
Expand Down Expand Up @@ -1548,5 +1549,4 @@ library console {
function log(address p0, address p1, address p2, address p3) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3));
}

}
317 changes: 155 additions & 162 deletions packages/hardhat-core/scripts/console-library-generator.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,131 @@
import fs from "fs";
import { bytesToInt } from "@nomicfoundation/ethereumjs-util";
import fs from "node:fs";

import { keccak256 } from "../src/internal/util/keccak";

const functionPrefix = " function";
const functionBody =
") internal pure {" +
'\n _sendLogPayload(abi.encodeWithSignature("log(';
const functionSuffix = "));" + "\n }" + "\n" + "\n";

let logger =
"// ------------------------------------\n" +
"// This code was autogenerated using\n" +
"// scripts/console-library-generator.ts\n" +
"// ------------------------------------\n\n";

const singleTypes = [
"int256",
"uint256",
"string memory",
"bool",
"address",
"bytes memory",
];
for (let i = 0; i < singleTypes.length; i++) {
const singleType = singleTypes[i].replace(" memory", "");
const type = singleType.charAt(0).toUpperCase() + singleType.slice(1);
logger += "export const " + type + 'Ty = "' + type + '";\n';
function capitalize(s: string): string {
return s.length === 0 ? "" : s.charAt(0).toUpperCase() + s.slice(1);
}

const offset = singleTypes.length - 1;
for (let i = 1; i <= 32; i++) {
singleTypes[offset + i] = "bytes" + i.toString();
logger +=
"export const Bytes" + i.toString() + 'Ty = "Bytes' + i.toString() + '";\n';
/**
* Generates all permutations of the given length and number of different
* elements as an iterator of 0-based indices.
*/
function* genPermutations(elemCount: number, len: number) {
// We can think of a permutation as a number of base `elemCount`, i.e.
// each 'digit' is a number between 0 and `elemCount - 1`.
// Then, to generate all permutations, we simply need to linearly iterate
// from 0 to max number of permutations (elemCount ** len) and convert
// each number to a list of digits as per the base `elemCount`, see above.
const numberOfPermutations = elemCount ** len;
const dividers = Array(elemCount)
.fill(0)
.map((_, i) => elemCount ** i);

for (let number = 0; number < numberOfPermutations; number++) {
const params = Array(len)
.fill(0)
.map((_, i) => Math.floor(number / dividers[i]) % elemCount);
// Reverse, so that we keep the natural big-endian ordering, i.e.
// [0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], ...
params.reverse();

yield params;
}
}

type TypeName = { type: string; modifier?: "memory" };
type FnParam = TypeName & { name: string };

/** Computes the function selector for the given function with simple arguments. */
function selector({ name = "", params = [] as TypeName[] }) {
const sigParams = params.map((p) => p.type).join(",");
return keccak256(Buffer.from(`${name}(${sigParams})`)).slice(0, 4);
}

function toHex(value: Uint8Array) {
return "0x" + Buffer.from(value).toString("hex");
}

const types = ["uint256", "string memory", "bool", "address"];
/** The types for which we generate `logUint`, `logString`, etc. */
const SINGLE_TYPES = [
{ type: "int256" },
{ type: "uint256" },
{ type: "string", modifier: "memory" },
{ type: "bool" },
{ type: "address" },
{ type: "bytes", modifier: "memory" },
...Array.from({ length: 32 }, (_, i) => ({ type: `bytes${i + 1}` })),
] as const;

/** The types for which we generate a `log` function with all possible
combinations of up to 4 arguments. */
const TYPES = [
{ type: "uint256" },
{ type: "string", modifier: "memory" },
{ type: "bool" },
{ type: "address" },
] as const;

/** A list of `console.log*` functions that we want to generate. */
const CONSOLE_LOG_FUNCTIONS =
// Basic `log()` function
[{ name: "log", params: [] as FnParam[] }]
// Generate single parameter functions that are type-suffixed for
// backwards-compatibility, e.g. logInt, logUint, logString, etc.
.concat(
SINGLE_TYPES.map((single) => {
const param = { ...single, name: "p0" };
const nameSuffix = capitalize(param.type.replace("int256", "int"));

return {
name: `log${nameSuffix}`,
params: [param],
};
})
)
// Also generate the function definitions for `log` for permutations of
// up to 4 parameters using the `types` (uint256, string, bool, address).
.concat(
[...Array(4)].flatMap((_, paramCount) => {
return Array.from(
genPermutations(TYPES.length, paramCount + 1),
(permutation) => ({
name: "log",
params: permutation.map((typeIndex, i) => ({
...TYPES[typeIndex],
name: `p${i}`,
})),
})
);
})
);

/** Maps from a 4-byte function selector to a signature (argument types) */
const CONSOLE_LOG_SIGNATURES: Map<string, string[]> =
CONSOLE_LOG_FUNCTIONS.reduce((acc, { params }) => {
// We always use `log` for the selector, even if it's logUint, for example.
const signature = toHex(selector({ name: "log", params }));
const types = params.map((p) => p.type);
acc.set(signature, types);

// For backwards compatibility, we additionally support the (invalid)
// selectors that contain the `int`/`uint` aliases in the selector calculation.
if (params.some((p) => ["uint256", "int256"].includes(p.type))) {
const aliased = params.map((p) => ({
...p,
type: p.type.replace("int256", "int"),
}));

const signature = toHex(selector({ name: "log", params: aliased }));
acc.set(signature, types);
}

return acc;
}, new Map());

let consoleSolFile = `// SPDX-License-Identifier: MIT
// Finally, render and save the console.sol and logger.ts files
const consoleSolFile = `\
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
library console {
Expand Down Expand Up @@ -74,140 +161,46 @@ library console {
_castToPure(_sendLogPayloadImplementation)(payload);
}
function log() internal pure {
_sendLogPayload(abi.encodeWithSignature("log()"));
${CONSOLE_LOG_FUNCTIONS.map(({ name, params }) => {
let fnParams = params
.map((p) => `${p.type}${p.modifier ? ` ${p.modifier}` : ""} ${p.name}`)
.join(", ");
let sig = params.map((p) => p.type).join(",");
let passed = params.map((p) => p.name).join(", ");
let passedArgs = passed.length > 0 ? `, ${passed}` : "";
return `\
function ${name}(${fnParams}) internal pure {
_sendLogPayload(abi.encodeWithSignature("log(${sig})"${passedArgs}));
}
`;

logger +=
"\n/** Maps from a 4-byte function selector to a signature (argument types) */\n" +
"export const CONSOLE_LOG_SIGNATURES: Record<number, string[]> = {\n";

// Add the empty log() first
const sigInt = bytesToInt(keccak256(Buffer.from("log" + "()")).slice(0, 4));
logger += " " + sigInt + ": [],\n";

for (let i = 0; i < singleTypes.length; i++) {
const type = singleTypes[i].replace(" memory", "");

// use logInt and logUint as function names for backwards-compatibility
const typeAliasedInt = type.replace("int256", "int");
const nameSuffix =
typeAliasedInt.charAt(0).toUpperCase() + typeAliasedInt.slice(1);

const sigInt = bytesToInt(
keccak256(Buffer.from("log" + "(" + type + ")")).slice(0, 4)
);
logger +=
" " +
sigInt +
": [" +
type.charAt(0).toUpperCase() +
type.slice(1) +
"Ty],\n";

const sigIntAliasedInt = bytesToInt(
keccak256(Buffer.from("log" + "(" + typeAliasedInt + ")")).slice(0, 4)
);
if (sigIntAliasedInt !== sigInt) {
logger +=
" " +
sigIntAliasedInt +
": [" +
type.charAt(0).toUpperCase() +
type.slice(1) +
"Ty],\n";
}

consoleSolFile +=
functionPrefix +
" log" +
nameSuffix +
"(" +
singleTypes[i] +
" p0" +
functionBody +
type +
')", ' +
"p0" +
functionSuffix;
}

const maxNumberOfParameters = 4;
const numberOfPermutations: Record<number, number> = {};
const dividers: Record<number, number> = {};
const paramsNames: Record<number, string[]> = {};

for (let i = 0; i < maxNumberOfParameters; i++) {
dividers[i] = Math.pow(maxNumberOfParameters, i);
numberOfPermutations[i] = Math.pow(maxNumberOfParameters, i + 1);

paramsNames[i] = [];
for (let j = 0; j <= i; j++) {
paramsNames[i][j] = "p" + j.toString();
}
}

for (let i = 0; i < maxNumberOfParameters; i++) {
for (let j = 0; j < numberOfPermutations[i]; j++) {
const params = [];

for (let k = 0; k <= i; k++) {
params.push(types[Math.floor(j / dividers[k]) % types.length]);
}
params.reverse();

let sigParams = [];
let sigParamsAliasedInt = [];
let constParams = [];

let input = "";
let internalParamsNames = [];
for (let k = 0; k <= i; k++) {
input += params[k] + " " + paramsNames[i][k] + ", ";
internalParamsNames.push(paramsNames[i][k]);

let param = params[k].replace(" memory", "");
let paramAliasedInt = param.replace("int256", "int");
sigParams.push(param);
sigParamsAliasedInt.push(paramAliasedInt);
constParams.push(param.charAt(0).toUpperCase() + param.slice(1) + "Ty");
}

consoleSolFile +=
functionPrefix +
" log(" +
input.substr(0, input.length - 2) +
functionBody +
sigParams.join(",") +
')", ' +
internalParamsNames.join(", ") +
functionSuffix;

if (sigParams.length !== 1) {
const sigInt = bytesToInt(
keccak256(Buffer.from("log(" + sigParams.join(",") + ")")).slice(0, 4)
);
logger += " " + sigInt + ": [" + constParams.join(", ") + "],\n";

const sigIntAliasedInt = bytesToInt(
keccak256(
Buffer.from("log(" + sigParamsAliasedInt.join(",") + ")")
).slice(0, 4)
);
if (sigIntAliasedInt !== sigInt) {
logger +=
" " + sigIntAliasedInt + ": [" + constParams.join(", ") + "],\n";
}
}
}
}).join("\n")}\
}
`;

consoleSolFile += "}\n";
logger = logger + "};\n";
const loggerFile = `\
// ------------------------------------
// This code was autogenerated using
// scripts/console-library-generator.ts
// ------------------------------------
${Array.from(SINGLE_TYPES.map((param) => capitalize(param.type)))
.map((type) => `export const ${type}Ty = "${type}";`)
.join("\n")}
/** Maps from a 4-byte function selector to a signature (argument types) */
export const CONSOLE_LOG_SIGNATURES: Record<number, string[]> = {
${Array.from(CONSOLE_LOG_SIGNATURES)
.map(([sig, types]) => {
const typeNames = types.map((type) => `${capitalize(type)}Ty`).join(", ");
return ` ${sig}: [${typeNames}],`;
})
.join("\n")}
};
`;

fs.writeFileSync(__dirname + "/../console.sol", consoleSolFile);
fs.writeFileSync(
__dirname + "/../src/internal/hardhat-network/stack-traces/logger.ts",
logger
loggerFile
);
fs.writeFileSync(__dirname + "/../console.sol", consoleSolFile);
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export class ConsoleLogger {
return util.format(...args);
}

/** Decodes a calldata buffer into string arguments for a console log. */
private static _maybeConsoleLog(
calldata: Buffer
): ConsoleLogArgs | undefined {
Expand Down Expand Up @@ -118,7 +119,7 @@ export class ConsoleLogger {
return decodedArgs;
}

/** Decodes parameters from `data` according to `types` into their string representation. */
/** Decodes calldata parameters from `data` according to `types` into their string representation. */
private static _decode(data: Buffer, types: string[]): string[] {
return types.map((type, i) => {
const position: number = i * 32;
Expand Down
Loading

0 comments on commit f93c447

Please sign in to comment.