Skip to content

Commit 4e747da

Browse files
committed
build/npm: add TS transformation that optimise for-of on arrays
Context: I wanted to try more automatic alternative to #3687 that allows us to continue using ES6 features without perfomance lost.
1 parent c9f968b commit 4e747da

File tree

3 files changed

+180
-1
lines changed

3 files changed

+180
-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 * as ts from 'typescript';
66

77
import { addExtensionToImportPaths } from './add-extension-to-import-paths';
88
import { inlineInvariant } from './inline-invariant';
9+
import { optimiseForOf } from './optimise-for-of';
910
import {
1011
localRepoPath,
1112
readdirRecursive,
@@ -55,6 +56,7 @@ tsHost.writeFile = (filepath, body) => {
5556

5657
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
5758
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
59+
before: [optimiseForOf(tsProgram)],
5860
after: [addExtensionToImportPaths({ extension: '.js' }), inlineInvariant],
5961
});
6062
assert(

resources/optimise-for-of.ts

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

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)