Skip to content

Commit 2cffe00

Browse files
committed
Revive for..of optimization
1 parent 4dfae39 commit 2cffe00

File tree

3 files changed

+190
-1
lines changed

3 files changed

+190
-1
lines changed

resources/build-npm.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ts from 'typescript';
66

77
import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js';
88
import { inlineInvariant } from './inline-invariant.js';
9+
import { optimizeForOf } from './optimize-for-of.js';
910
import {
1011
prettify,
1112
readPackageJSON,
@@ -145,6 +146,7 @@ function emitTSFiles(options: {
145146

146147
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
147148
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
149+
before: [optimizeForOf(tsProgram)],
148150
after: [changeExtensionInImportPaths({ extension }), inlineInvariant],
149151
});
150152
assert(

resources/optimize-for-of.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import assert from 'node:assert';
2+
3+
import ts from 'typescript';
4+
5+
/**
6+
* The following ES6 code:
7+
*
8+
* ```
9+
* for (let v of expr) { }
10+
* ```
11+
*
12+
* should be emitted as
13+
*
14+
* ```
15+
* for (let _i = 0, _a = expr; _i < _a.length; _i++) {
16+
* let v = _a[_i];
17+
* }
18+
* ```
19+
*
20+
* where _a and _i are temps emitted to capture the RHS and the counter, respectively.
21+
* When the left hand side is a let/const, the v is renamed if there is another v in scope.
22+
* Note that all assignments to the LHS are emitted in the body, including all destructuring.
23+
*
24+
* Code is based on TS ES5 transpilation:
25+
* https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/transformers/es2015.ts#L2521
26+
*/
27+
export function optimizeForOf(program: ts.Program) {
28+
const transformer: ts.TransformerFactory<ts.SourceFile> = (
29+
context: ts.TransformationContext,
30+
) => {
31+
const typeChecker = program.getTypeChecker();
32+
const { factory } = context;
33+
34+
return visitSourceFile;
35+
36+
function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile {
37+
return ts.visitNode(sourceFile, visitNode) as ts.SourceFile;
38+
39+
function visitNode(node: ts.Node): ts.Node {
40+
if (isArrayForOfStatement(node)) {
41+
return convertForOfStatementForArray(node);
42+
}
43+
return ts.visitEachChild(node, visitNode, context);
44+
}
45+
46+
function isArrayForOfStatement(node: ts.Node): node is ts.ForOfStatement {
47+
if (!ts.isForOfStatement(node) || node.awaitModifier != null) {
48+
return false;
49+
}
50+
51+
const { expression } = node;
52+
53+
const expressionType = typeChecker.getTypeAtLocation(expression);
54+
55+
for (const subType of unionTypeParts(expressionType)) {
56+
assert(
57+
!(subType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)),
58+
'Can not use any or unknown values in for-of loop: ' +
59+
nodeLocationString(node),
60+
);
61+
62+
if (subType.flags & ts.TypeFlags.StringLike) {
63+
continue;
64+
}
65+
66+
const typeName = subType.getSymbol()?.getName();
67+
assert(typeName != null);
68+
69+
if (typeName === 'Array' || typeName === 'ReadonlyArray') {
70+
continue;
71+
}
72+
73+
return false;
74+
}
75+
76+
return true;
77+
}
78+
79+
function nodeLocationString(node: ts.Node): string {
80+
const position = sourceFile.getLineAndCharacterOfPosition(
81+
node.getStart(),
82+
);
83+
return sourceFile.fileName + ':' + position.line;
84+
}
85+
86+
function unionTypeParts(type: ts.Type): ReadonlyArray<ts.Type> {
87+
return isUnionType(type) ? type.types : [type];
88+
}
89+
90+
function isUnionType(type: ts.Type): type is ts.UnionType {
91+
return (type.flags & ts.TypeFlags.Union) !== 0;
92+
}
93+
94+
function convertForOfStatementForArray(
95+
forOfNode: ts.ForOfStatement,
96+
): ts.Statement {
97+
const counter = factory.createLoopVariable();
98+
const forDeclarations = [
99+
factory.createVariableDeclaration(
100+
counter,
101+
undefined, // exclamationToken
102+
undefined, // type
103+
factory.createNumericLiteral(0),
104+
),
105+
];
106+
107+
// In the case where the user wrote an identifier as the RHS, like this:
108+
//
109+
// for (let v of arr) { }
110+
//
111+
// we don't want to emit a temporary variable for the RHS, just use it directly.
112+
let rhsReference;
113+
if (ts.isIdentifier(forOfNode.expression)) {
114+
rhsReference = forOfNode.expression;
115+
} else {
116+
rhsReference = factory.createTempVariable(
117+
undefined, // recordTempVariable
118+
);
119+
forDeclarations.push(
120+
factory.createVariableDeclaration(
121+
rhsReference,
122+
undefined, // exclamationToken
123+
undefined, // type
124+
forOfNode.expression,
125+
),
126+
);
127+
}
128+
129+
const forInitiliazer = factory.createVariableDeclarationList(
130+
forDeclarations,
131+
ts.NodeFlags.Let,
132+
);
133+
134+
const forCondition = factory.createLessThan(
135+
counter,
136+
factory.createPropertyAccessExpression(rhsReference, 'length'),
137+
);
138+
const forIncrementor = factory.createPostfixIncrement(counter);
139+
140+
assert(ts.isVariableDeclarationList(forOfNode.initializer));
141+
// It will use rhsIterationValue _a[_i] as the initializer.
142+
const itemAssignment = convertForOfInitializer(
143+
forOfNode.initializer,
144+
factory.createElementAccessExpression(rhsReference, counter),
145+
);
146+
147+
assert(ts.isBlock(forOfNode.statement));
148+
const forBody = factory.updateBlock(forOfNode.statement, [
149+
itemAssignment,
150+
...forOfNode.statement.statements,
151+
]);
152+
153+
return factory.createForStatement(
154+
forInitiliazer,
155+
forCondition,
156+
forIncrementor,
157+
forBody,
158+
);
159+
}
160+
161+
function convertForOfInitializer(
162+
forOfDeclarationList: ts.VariableDeclarationList,
163+
itemAccessExpression: ts.Expression,
164+
) {
165+
assert(forOfDeclarationList.declarations.length === 1);
166+
const [forOfDeclaration] = forOfDeclarationList.declarations;
167+
168+
const updatedDeclaration = factory.updateVariableDeclaration(
169+
forOfDeclaration,
170+
forOfDeclaration.name,
171+
forOfDeclaration.exclamationToken,
172+
forOfDeclaration.type,
173+
itemAccessExpression,
174+
);
175+
176+
return factory.createVariableStatement(
177+
undefined, // modifiers
178+
factory.updateVariableDeclarationList(forOfDeclarationList, [
179+
updatedDeclaration,
180+
]),
181+
);
182+
}
183+
}
184+
};
185+
186+
return transformer;
187+
}

src/language/visitor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function visit(
189189
let inArray = Array.isArray(root);
190190
let keys: any = [root];
191191
let index = -1;
192-
let edits = [];
192+
let edits: Array<any> = [];
193193
let node: any = root;
194194
let key: any = undefined;
195195
let parent: any = undefined;

0 commit comments

Comments
 (0)