Skip to content

Commit f93c447

Browse files
authored
Merge pull request #5759 from NomicFoundation/refactor/streamline-console-library-generator
refactor: Streamline `console.sol` generation
2 parents 74b1bfe + 5b410f9 commit f93c447

File tree

4 files changed

+754
-760
lines changed

4 files changed

+754
-760
lines changed

packages/hardhat-core/console.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ library console {
3737
function log() internal pure {
3838
_sendLogPayload(abi.encodeWithSignature("log()"));
3939
}
40+
4041
function logInt(int256 p0) internal pure {
4142
_sendLogPayload(abi.encodeWithSignature("log(int256)", p0));
4243
}
@@ -1548,5 +1549,4 @@ library console {
15481549
function log(address p0, address p1, address p2, address p3) internal pure {
15491550
_sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3));
15501551
}
1551-
15521552
}
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,131 @@
1-
import fs from "fs";
2-
import { bytesToInt } from "@nomicfoundation/ethereumjs-util";
1+
import fs from "node:fs";
32

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

6-
const functionPrefix = " function";
7-
const functionBody =
8-
") internal pure {" +
9-
'\n _sendLogPayload(abi.encodeWithSignature("log(';
10-
const functionSuffix = "));" + "\n }" + "\n" + "\n";
11-
12-
let logger =
13-
"// ------------------------------------\n" +
14-
"// This code was autogenerated using\n" +
15-
"// scripts/console-library-generator.ts\n" +
16-
"// ------------------------------------\n\n";
17-
18-
const singleTypes = [
19-
"int256",
20-
"uint256",
21-
"string memory",
22-
"bool",
23-
"address",
24-
"bytes memory",
25-
];
26-
for (let i = 0; i < singleTypes.length; i++) {
27-
const singleType = singleTypes[i].replace(" memory", "");
28-
const type = singleType.charAt(0).toUpperCase() + singleType.slice(1);
29-
logger += "export const " + type + 'Ty = "' + type + '";\n';
5+
function capitalize(s: string): string {
6+
return s.length === 0 ? "" : s.charAt(0).toUpperCase() + s.slice(1);
307
}
318

32-
const offset = singleTypes.length - 1;
33-
for (let i = 1; i <= 32; i++) {
34-
singleTypes[offset + i] = "bytes" + i.toString();
35-
logger +=
36-
"export const Bytes" + i.toString() + 'Ty = "Bytes' + i.toString() + '";\n';
9+
/**
10+
* Generates all permutations of the given length and number of different
11+
* elements as an iterator of 0-based indices.
12+
*/
13+
function* genPermutations(elemCount: number, len: number) {
14+
// We can think of a permutation as a number of base `elemCount`, i.e.
15+
// each 'digit' is a number between 0 and `elemCount - 1`.
16+
// Then, to generate all permutations, we simply need to linearly iterate
17+
// from 0 to max number of permutations (elemCount ** len) and convert
18+
// each number to a list of digits as per the base `elemCount`, see above.
19+
const numberOfPermutations = elemCount ** len;
20+
const dividers = Array(elemCount)
21+
.fill(0)
22+
.map((_, i) => elemCount ** i);
23+
24+
for (let number = 0; number < numberOfPermutations; number++) {
25+
const params = Array(len)
26+
.fill(0)
27+
.map((_, i) => Math.floor(number / dividers[i]) % elemCount);
28+
// Reverse, so that we keep the natural big-endian ordering, i.e.
29+
// [0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2], ...
30+
params.reverse();
31+
32+
yield params;
33+
}
34+
}
35+
36+
type TypeName = { type: string; modifier?: "memory" };
37+
type FnParam = TypeName & { name: string };
38+
39+
/** Computes the function selector for the given function with simple arguments. */
40+
function selector({ name = "", params = [] as TypeName[] }) {
41+
const sigParams = params.map((p) => p.type).join(",");
42+
return keccak256(Buffer.from(`${name}(${sigParams})`)).slice(0, 4);
43+
}
44+
45+
function toHex(value: Uint8Array) {
46+
return "0x" + Buffer.from(value).toString("hex");
3747
}
3848

39-
const types = ["uint256", "string memory", "bool", "address"];
49+
/** The types for which we generate `logUint`, `logString`, etc. */
50+
const SINGLE_TYPES = [
51+
{ type: "int256" },
52+
{ type: "uint256" },
53+
{ type: "string", modifier: "memory" },
54+
{ type: "bool" },
55+
{ type: "address" },
56+
{ type: "bytes", modifier: "memory" },
57+
...Array.from({ length: 32 }, (_, i) => ({ type: `bytes${i + 1}` })),
58+
] as const;
59+
60+
/** The types for which we generate a `log` function with all possible
61+
combinations of up to 4 arguments. */
62+
const TYPES = [
63+
{ type: "uint256" },
64+
{ type: "string", modifier: "memory" },
65+
{ type: "bool" },
66+
{ type: "address" },
67+
] as const;
68+
69+
/** A list of `console.log*` functions that we want to generate. */
70+
const CONSOLE_LOG_FUNCTIONS =
71+
// Basic `log()` function
72+
[{ name: "log", params: [] as FnParam[] }]
73+
// Generate single parameter functions that are type-suffixed for
74+
// backwards-compatibility, e.g. logInt, logUint, logString, etc.
75+
.concat(
76+
SINGLE_TYPES.map((single) => {
77+
const param = { ...single, name: "p0" };
78+
const nameSuffix = capitalize(param.type.replace("int256", "int"));
79+
80+
return {
81+
name: `log${nameSuffix}`,
82+
params: [param],
83+
};
84+
})
85+
)
86+
// Also generate the function definitions for `log` for permutations of
87+
// up to 4 parameters using the `types` (uint256, string, bool, address).
88+
.concat(
89+
[...Array(4)].flatMap((_, paramCount) => {
90+
return Array.from(
91+
genPermutations(TYPES.length, paramCount + 1),
92+
(permutation) => ({
93+
name: "log",
94+
params: permutation.map((typeIndex, i) => ({
95+
...TYPES[typeIndex],
96+
name: `p${i}`,
97+
})),
98+
})
99+
);
100+
})
101+
);
102+
103+
/** Maps from a 4-byte function selector to a signature (argument types) */
104+
const CONSOLE_LOG_SIGNATURES: Map<string, string[]> =
105+
CONSOLE_LOG_FUNCTIONS.reduce((acc, { params }) => {
106+
// We always use `log` for the selector, even if it's logUint, for example.
107+
const signature = toHex(selector({ name: "log", params }));
108+
const types = params.map((p) => p.type);
109+
acc.set(signature, types);
110+
111+
// For backwards compatibility, we additionally support the (invalid)
112+
// selectors that contain the `int`/`uint` aliases in the selector calculation.
113+
if (params.some((p) => ["uint256", "int256"].includes(p.type))) {
114+
const aliased = params.map((p) => ({
115+
...p,
116+
type: p.type.replace("int256", "int"),
117+
}));
118+
119+
const signature = toHex(selector({ name: "log", params: aliased }));
120+
acc.set(signature, types);
121+
}
122+
123+
return acc;
124+
}, new Map());
40125

41-
let consoleSolFile = `// SPDX-License-Identifier: MIT
126+
// Finally, render and save the console.sol and logger.ts files
127+
const consoleSolFile = `\
128+
// SPDX-License-Identifier: MIT
42129
pragma solidity >=0.4.22 <0.9.0;
43130
44131
library console {
@@ -74,140 +161,46 @@ library console {
74161
_castToPure(_sendLogPayloadImplementation)(payload);
75162
}
76163
77-
function log() internal pure {
78-
_sendLogPayload(abi.encodeWithSignature("log()"));
164+
${CONSOLE_LOG_FUNCTIONS.map(({ name, params }) => {
165+
let fnParams = params
166+
.map((p) => `${p.type}${p.modifier ? ` ${p.modifier}` : ""} ${p.name}`)
167+
.join(", ");
168+
let sig = params.map((p) => p.type).join(",");
169+
let passed = params.map((p) => p.name).join(", ");
170+
let passedArgs = passed.length > 0 ? `, ${passed}` : "";
171+
172+
return `\
173+
function ${name}(${fnParams}) internal pure {
174+
_sendLogPayload(abi.encodeWithSignature("log(${sig})"${passedArgs}));
79175
}
80176
`;
81-
82-
logger +=
83-
"\n/** Maps from a 4-byte function selector to a signature (argument types) */\n" +
84-
"export const CONSOLE_LOG_SIGNATURES: Record<number, string[]> = {\n";
85-
86-
// Add the empty log() first
87-
const sigInt = bytesToInt(keccak256(Buffer.from("log" + "()")).slice(0, 4));
88-
logger += " " + sigInt + ": [],\n";
89-
90-
for (let i = 0; i < singleTypes.length; i++) {
91-
const type = singleTypes[i].replace(" memory", "");
92-
93-
// use logInt and logUint as function names for backwards-compatibility
94-
const typeAliasedInt = type.replace("int256", "int");
95-
const nameSuffix =
96-
typeAliasedInt.charAt(0).toUpperCase() + typeAliasedInt.slice(1);
97-
98-
const sigInt = bytesToInt(
99-
keccak256(Buffer.from("log" + "(" + type + ")")).slice(0, 4)
100-
);
101-
logger +=
102-
" " +
103-
sigInt +
104-
": [" +
105-
type.charAt(0).toUpperCase() +
106-
type.slice(1) +
107-
"Ty],\n";
108-
109-
const sigIntAliasedInt = bytesToInt(
110-
keccak256(Buffer.from("log" + "(" + typeAliasedInt + ")")).slice(0, 4)
111-
);
112-
if (sigIntAliasedInt !== sigInt) {
113-
logger +=
114-
" " +
115-
sigIntAliasedInt +
116-
": [" +
117-
type.charAt(0).toUpperCase() +
118-
type.slice(1) +
119-
"Ty],\n";
120-
}
121-
122-
consoleSolFile +=
123-
functionPrefix +
124-
" log" +
125-
nameSuffix +
126-
"(" +
127-
singleTypes[i] +
128-
" p0" +
129-
functionBody +
130-
type +
131-
')", ' +
132-
"p0" +
133-
functionSuffix;
134-
}
135-
136-
const maxNumberOfParameters = 4;
137-
const numberOfPermutations: Record<number, number> = {};
138-
const dividers: Record<number, number> = {};
139-
const paramsNames: Record<number, string[]> = {};
140-
141-
for (let i = 0; i < maxNumberOfParameters; i++) {
142-
dividers[i] = Math.pow(maxNumberOfParameters, i);
143-
numberOfPermutations[i] = Math.pow(maxNumberOfParameters, i + 1);
144-
145-
paramsNames[i] = [];
146-
for (let j = 0; j <= i; j++) {
147-
paramsNames[i][j] = "p" + j.toString();
148-
}
149-
}
150-
151-
for (let i = 0; i < maxNumberOfParameters; i++) {
152-
for (let j = 0; j < numberOfPermutations[i]; j++) {
153-
const params = [];
154-
155-
for (let k = 0; k <= i; k++) {
156-
params.push(types[Math.floor(j / dividers[k]) % types.length]);
157-
}
158-
params.reverse();
159-
160-
let sigParams = [];
161-
let sigParamsAliasedInt = [];
162-
let constParams = [];
163-
164-
let input = "";
165-
let internalParamsNames = [];
166-
for (let k = 0; k <= i; k++) {
167-
input += params[k] + " " + paramsNames[i][k] + ", ";
168-
internalParamsNames.push(paramsNames[i][k]);
169-
170-
let param = params[k].replace(" memory", "");
171-
let paramAliasedInt = param.replace("int256", "int");
172-
sigParams.push(param);
173-
sigParamsAliasedInt.push(paramAliasedInt);
174-
constParams.push(param.charAt(0).toUpperCase() + param.slice(1) + "Ty");
175-
}
176-
177-
consoleSolFile +=
178-
functionPrefix +
179-
" log(" +
180-
input.substr(0, input.length - 2) +
181-
functionBody +
182-
sigParams.join(",") +
183-
')", ' +
184-
internalParamsNames.join(", ") +
185-
functionSuffix;
186-
187-
if (sigParams.length !== 1) {
188-
const sigInt = bytesToInt(
189-
keccak256(Buffer.from("log(" + sigParams.join(",") + ")")).slice(0, 4)
190-
);
191-
logger += " " + sigInt + ": [" + constParams.join(", ") + "],\n";
192-
193-
const sigIntAliasedInt = bytesToInt(
194-
keccak256(
195-
Buffer.from("log(" + sigParamsAliasedInt.join(",") + ")")
196-
).slice(0, 4)
197-
);
198-
if (sigIntAliasedInt !== sigInt) {
199-
logger +=
200-
" " + sigIntAliasedInt + ": [" + constParams.join(", ") + "],\n";
201-
}
202-
}
203-
}
177+
}).join("\n")}\
204178
}
179+
`;
205180

206-
consoleSolFile += "}\n";
207-
logger = logger + "};\n";
181+
const loggerFile = `\
182+
// ------------------------------------
183+
// This code was autogenerated using
184+
// scripts/console-library-generator.ts
185+
// ------------------------------------
186+
187+
${Array.from(SINGLE_TYPES.map((param) => capitalize(param.type)))
188+
.map((type) => `export const ${type}Ty = "${type}";`)
189+
.join("\n")}
190+
191+
/** Maps from a 4-byte function selector to a signature (argument types) */
192+
export const CONSOLE_LOG_SIGNATURES: Record<number, string[]> = {
193+
${Array.from(CONSOLE_LOG_SIGNATURES)
194+
.map(([sig, types]) => {
195+
const typeNames = types.map((type) => `${capitalize(type)}Ty`).join(", ");
196+
return ` ${sig}: [${typeNames}],`;
197+
})
198+
.join("\n")}
199+
};
200+
`;
208201

202+
fs.writeFileSync(__dirname + "/../console.sol", consoleSolFile);
209203
fs.writeFileSync(
210204
__dirname + "/../src/internal/hardhat-network/stack-traces/logger.ts",
211-
logger
205+
loggerFile
212206
);
213-
fs.writeFileSync(__dirname + "/../console.sol", consoleSolFile);

packages/hardhat-core/src/internal/hardhat-network/stack-traces/consoleLogger.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export class ConsoleLogger {
8282
return util.format(...args);
8383
}
8484

85+
/** Decodes a calldata buffer into string arguments for a console log. */
8586
private static _maybeConsoleLog(
8687
calldata: Buffer
8788
): ConsoleLogArgs | undefined {
@@ -118,7 +119,7 @@ export class ConsoleLogger {
118119
return decodedArgs;
119120
}
120121

121-
/** Decodes parameters from `data` according to `types` into their string representation. */
122+
/** Decodes calldata parameters from `data` according to `types` into their string representation. */
122123
private static _decode(data: Buffer, types: string[]): string[] {
123124
return types.map((type, i) => {
124125
const position: number = i * 32;

0 commit comments

Comments
 (0)