Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Streamline console.sol generation #5759

Merged
merged 5 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
Comment on lines +89 to +91
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[...Array(4)].flatMap((_, paramCount) => {
return Array.from(
genPermutations(TYPES.length, paramCount + 1),
[...Array(5)].flatMap((_, paramCount) => {
return Array.from(
genPermutations(TYPES.length, paramCount),

As a side-note, this base case can handle generating the empty log() just fine but shifted its order in the console.sol file. This is probably meaningless but I wanted to avoid making any changes; let me know if you'd like me to not explicitly handle log() and handle this with this code, instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, but yes, let's leave it as is.

(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