perf(packages/codegen): reduce megamorphic type reads in printForOfStatement (#27593)

Perf optimization. Continuation of [#27458](https://github.com/oxc-project/oxc/issues/27458).

The left side of a `for of` head can be any declaration or assignment target, so reading its `type` is a megamorphic load (a builtin call, which V8 does not deduplicate). Read it once in `printForOfStatement` and pass it to a single `forOfHeadNeedsWrapping`, which applies both the `let` and `async` rules while walking the leftmost spine, reading each level's `type` once.

Previously computed members read their object's `type` three times (via `withoutParens`). Adds a fixture and hand-built tests for `let`/`async` heads, including parenthesized ones, which only hand-built ASTs can reach.
This commit is contained in:
overlookmotel committed 2026-10-12 14:03:11 +00:00
1 parent b29f35eac3
commit 8d0ba201fd
3 files changed
+127 -24

No files matched your search

+44 -24
View File
@@ -1,6 +1,6 @@
// Statements.
import { typeAssertIs } from "../asserts.ts";
import { debugAssert, typeAssertIs } from "../asserts.ts";
import { printAssignmentTarget } from "./assignment_target.ts";
import { printBindingPattern } from "./binding_pattern.ts";
import {
@@ -32,7 +32,6 @@ import {
printImportDeclaration,
} from "./module.ts";
import { CTX_FORBID_IN, CTX_NONE } from "./operators.ts";
import { withoutParens } from "./parens.ts";
import { PREC_COMMA, PREC_LOWEST } from "./precedence.ts";
import { printString } from "./string.ts";
import {
@@ -752,14 +751,12 @@ function printForOfStatement(node: ESTree.ForOfStatement, state: State): void {
write(state, " (", CAT_OTHER);
const { left } = node;
if (left.type === "VariableDeclaration") {
const leftType = left.type;
if (leftType === "VariableDeclaration") {
printVariableDeclaration(left, state, CTX_NONE);
} else {
typeAssertIs<ESTree.Expression>(left);
const bare = withoutParens(left);
const wrap =
forOfHeadStartsWithLet(left)
|| (!node.await && bare.type === "Identifier" && bare.name === "async");
const wrap = forOfHeadNeedsWrapping(left, leftType, node.await !== true);
if (wrap) write(state, "(", CAT_OTHER);
printAssignmentTarget(left, state);
if (wrap) write(state, ")", CAT_CLOSE_BRACKET);
@@ -773,28 +770,51 @@ function printForOfStatement(node: ESTree.ForOfStatement, state: State): void {
}
/**
* The restriction is on the first token of the head rather than on the target as a whole,
* so this walks down the leftmost spine - through member objects and parens -
* to whatever prints first.
* Returns `true` if `left` needs wrapping in parentheses.
*
* This is the case if either:
* 1. The leading token when printing `left` would be `let`.
* 2. `left` is an identifier `async` and `wrapAsync` is `true`.
*
* `leftType` must be the `type` of `left`.
*
* The restriction for `let` is on the first token of the head rather than on the target as a whole,
* so this walks down the leftmost spine - through member objects and parens - to whatever prints first.
*/
function forOfHeadStartsWithLet(left: ESTree.Expression): boolean {
// Whether the leading token would be `let`.
function forOfHeadNeedsWrapping(
left: ESTree.AssignmentTarget | ESTree.Expression,
leftType: (ESTree.AssignmentTarget | ESTree.Expression)["type"],
wrapAsync: boolean,
): boolean {
debugAssert(leftType === left.type, "`leftType` must equal `left.type`");
let computed = false;
for (;;) {
switch (left.type) {
case "Identifier":
return left.name === "let";
switch (leftType) {
case "Identifier": {
// Computed member: `let[...]` is emitted as `(let)[...]`, so no longer starts with `let`.
// Any other identifier isn't `let` either, so return `false` for all of them.
// Non-computed member: If identifier name is `let` (`let.x`), it needs to be wrapped.
// Not part of a member expression: `let` needs to be wrapped. `async` does too if `wrapAsync` is set.
if (computed) return false;
const leftName = (left as ESTree.IdentifierReference).name;
if (leftName === "let") return true;
return wrapAsync && leftName === "async";
}
case "MemberExpression":
if (left.computed) {
const inner = withoutParens(left.object);
if (inner.type === "Identifier" && inner.name === "let") {
// `let[...]` is emitted as `(let)[...]`, so no longer starts with `let`
return false;
}
}
typeAssertIs<ESTree.MemberExpression>(left);
computed = left.computed;
left = left.object;
leftType = left.type;
// `async` only needs wrapping when it's a plain identifier e.g. `for ((async) of arr)`,
// but not when part of a `MemberExpression` e.g. `for (async.x of arr)`
wrapAsync = false;
break;
case "ParenthesizedExpression":
left = left.expression;
left = (left as ESTree.ParenthesizedExpression).expression;
leftType = left.type;
break;
default:
return false;
@@ -0,0 +1,8 @@
// A `for of` head cannot start with `let`, so these must stay parenthesized
for ((let).x of y);
for ((let.a).x of y);
for ((let).a[0] of y);
// `let[...]` prints as `(let)[...]`, which no longer starts with `let`, so these need no more parens
for ((let)[0] of y);
for ((let)[0].x of y);
+75
View File
@@ -63,6 +63,14 @@ const member = (object: ESTree.Expression, property: ESTree.IdentifierName) =>
computed: false,
optional: false,
});
const computedMember = (object: ESTree.Expression, property: ESTree.Expression) =>
node<ESTree.ComputedMemberExpression>({
type: "MemberExpression",
object,
property,
computed: true,
optional: false,
});
const call = (callee: ESTree.Expression, args: ESTree.Argument[] = []) =>
node<ESTree.CallExpression>({
type: "CallExpression",
@@ -85,6 +93,18 @@ const arr = (...elements: ESTree.ArrayExpressionElement[]) =>
node<ESTree.ArrayExpression>({ type: "ArrayExpression", elements });
const ret = (argument: ESTree.Expression) =>
node<ESTree.ReturnStatement>({ type: "ReturnStatement", argument });
const paren = (expression: ESTree.Expression) =>
node<ESTree.ParenthesizedExpression>({ type: "ParenthesizedExpression", expression });
// `left` is typed as any expression, because these tests put parens around it,
// which `ForStatementLeft` does not allow
const forOf = (left: ESTree.Expression, isAwait = false) =>
node<ESTree.ForOfStatement>({
type: "ForOfStatement",
await: isAwait,
left: left as ESTree.ForStatementLeft,
right: id("y"),
body: node<ESTree.EmptyStatement>({ type: "EmptyStatement" }),
});
// One expression statement, which is how nearly every case below is shaped
const e = (expression: ESTree.Expression) => program(stmt(expression));
@@ -404,6 +424,61 @@ describe("space before identifiers", () => {
]);
});
// A `for of` head cannot start with `let`, or with `async of`, so such a target must be wrapped in parens.
// - A computed member on `let` needs no more, because it prints as `(let)[...]` anyway.
// - A member on `async` needs none, because `async.x of` and `async["x"] of` do not start with `async of`.
// - Parens in the AST are transparent when printing, so a `ParenthesizedExpression` around the target,
// or around its object, changes none of this.
//
// `oxc-parser` never produces a `ParenthesizedExpression` in a `for of` head, even with
// `preserveParens: true`, but Acorn's `preserveParens` does. So only a hand-built AST reaches those cases.
describe("for-of heads starting with `async` or `let`", () => {
checkCases([
["async", program(forOf(id("async"))), "for ((async) of y);\n"],
["paren-async", program(forOf(paren(id("async")))), "for ((async) of y);\n"],
["paren-paren-async", program(forOf(paren(paren(id("async"))))), "for ((async) of y);\n"],
// `for await (async of ...)` is allowed
["await-paren-async", program(forOf(paren(id("async")), true)), "for await (async of y);\n"],
[
"paren-async-member",
program(forOf(member(paren(id("async")), id("x")))),
"for (async.x of y);\n",
],
["paren-let", program(forOf(paren(id("let")))), "for ((let) of y);\n"],
// Computed members
[
"async-computed",
program(forOf(computedMember(id("async"), str("x")))),
'for (async["x"] of y);\n',
],
[
"let-computed",
program(forOf(computedMember(id("let"), str("x")))),
'for ((let)["x"] of y);\n',
],
[
"paren-async-computed",
program(forOf(computedMember(paren(id("async")), str("x")))),
'for (async["x"] of y);\n',
],
[
"paren-let-computed",
program(forOf(computedMember(paren(id("let")), str("x")))),
'for ((let)["x"] of y);\n',
],
[
"paren-whole-async-computed",
program(forOf(paren(computedMember(id("async"), str("x"))))),
'for (async["x"] of y);\n',
],
[
"paren-whole-let-computed",
program(forOf(paren(computedMember(id("let"), str("x"))))),
'for ((let)["x"] of y);\n',
],
]);
});
// The three `export default` declaration forms - interface, class and function - each end with a
// newline, so whatever follows starts on its own line. `export default interface` printed without
// one until `oxc_codegen` was fixed, gluing the next statement onto the closing brace.