From 2cffe0086324017dcbd39e56be6ab38e57ab7c56 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 29 Sep 2024 19:14:51 +0200 Subject: [PATCH 1/2] Revive for..of optimization --- resources/build-npm.ts | 2 + resources/optimize-for-of.ts | 187 +++++++++++++++++++++++++++++++++++ src/language/visitor.ts | 2 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 resources/optimize-for-of.ts diff --git a/resources/build-npm.ts b/resources/build-npm.ts index a68ce9bbe7..d75477364d 100644 --- a/resources/build-npm.ts +++ b/resources/build-npm.ts @@ -6,6 +6,7 @@ import ts from 'typescript'; import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; import { inlineInvariant } from './inline-invariant.js'; +import { optimizeForOf } from './optimize-for-of.js'; import { prettify, readPackageJSON, @@ -145,6 +146,7 @@ function emitTSFiles(options: { const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost); const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, { + before: [optimizeForOf(tsProgram)], after: [changeExtensionInImportPaths({ extension }), inlineInvariant], }); assert( diff --git a/resources/optimize-for-of.ts b/resources/optimize-for-of.ts new file mode 100644 index 0000000000..c22354d3e1 --- /dev/null +++ b/resources/optimize-for-of.ts @@ -0,0 +1,187 @@ +import assert from 'node:assert'; + +import ts from 'typescript'; + +/** + * The following ES6 code: + * + * ``` + * for (let v of expr) { } + * ``` + * + * should be emitted as + * + * ``` + * for (let _i = 0, _a = expr; _i < _a.length; _i++) { + * let v = _a[_i]; + * } + * ``` + * + * where _a and _i are temps emitted to capture the RHS and the counter, respectively. + * When the left hand side is a let/const, the v is renamed if there is another v in scope. + * Note that all assignments to the LHS are emitted in the body, including all destructuring. + * + * Code is based on TS ES5 transpilation: + * https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/transformers/es2015.ts#L2521 + */ +export function optimizeForOf(program: ts.Program) { + const transformer: ts.TransformerFactory = ( + context: ts.TransformationContext, + ) => { + const typeChecker = program.getTypeChecker(); + const { factory } = context; + + return visitSourceFile; + + function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile { + return ts.visitNode(sourceFile, visitNode) as ts.SourceFile; + + function visitNode(node: ts.Node): ts.Node { + if (isArrayForOfStatement(node)) { + return convertForOfStatementForArray(node); + } + return ts.visitEachChild(node, visitNode, context); + } + + function isArrayForOfStatement(node: ts.Node): node is ts.ForOfStatement { + if (!ts.isForOfStatement(node) || node.awaitModifier != null) { + return false; + } + + const { expression } = node; + + const expressionType = typeChecker.getTypeAtLocation(expression); + + for (const subType of unionTypeParts(expressionType)) { + assert( + !(subType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)), + 'Can not use any or unknown values in for-of loop: ' + + nodeLocationString(node), + ); + + if (subType.flags & ts.TypeFlags.StringLike) { + continue; + } + + const typeName = subType.getSymbol()?.getName(); + assert(typeName != null); + + if (typeName === 'Array' || typeName === 'ReadonlyArray') { + continue; + } + + return false; + } + + return true; + } + + function nodeLocationString(node: ts.Node): string { + const position = sourceFile.getLineAndCharacterOfPosition( + node.getStart(), + ); + return sourceFile.fileName + ':' + position.line; + } + + function unionTypeParts(type: ts.Type): ReadonlyArray { + return isUnionType(type) ? type.types : [type]; + } + + function isUnionType(type: ts.Type): type is ts.UnionType { + return (type.flags & ts.TypeFlags.Union) !== 0; + } + + function convertForOfStatementForArray( + forOfNode: ts.ForOfStatement, + ): ts.Statement { + const counter = factory.createLoopVariable(); + const forDeclarations = [ + factory.createVariableDeclaration( + counter, + undefined, // exclamationToken + undefined, // type + factory.createNumericLiteral(0), + ), + ]; + + // In the case where the user wrote an identifier as the RHS, like this: + // + // for (let v of arr) { } + // + // we don't want to emit a temporary variable for the RHS, just use it directly. + let rhsReference; + if (ts.isIdentifier(forOfNode.expression)) { + rhsReference = forOfNode.expression; + } else { + rhsReference = factory.createTempVariable( + undefined, // recordTempVariable + ); + forDeclarations.push( + factory.createVariableDeclaration( + rhsReference, + undefined, // exclamationToken + undefined, // type + forOfNode.expression, + ), + ); + } + + const forInitiliazer = factory.createVariableDeclarationList( + forDeclarations, + ts.NodeFlags.Let, + ); + + const forCondition = factory.createLessThan( + counter, + factory.createPropertyAccessExpression(rhsReference, 'length'), + ); + const forIncrementor = factory.createPostfixIncrement(counter); + + assert(ts.isVariableDeclarationList(forOfNode.initializer)); + // It will use rhsIterationValue _a[_i] as the initializer. + const itemAssignment = convertForOfInitializer( + forOfNode.initializer, + factory.createElementAccessExpression(rhsReference, counter), + ); + + assert(ts.isBlock(forOfNode.statement)); + const forBody = factory.updateBlock(forOfNode.statement, [ + itemAssignment, + ...forOfNode.statement.statements, + ]); + + return factory.createForStatement( + forInitiliazer, + forCondition, + forIncrementor, + forBody, + ); + } + + function convertForOfInitializer( + forOfDeclarationList: ts.VariableDeclarationList, + itemAccessExpression: ts.Expression, + ) { + assert(forOfDeclarationList.declarations.length === 1); + const [forOfDeclaration] = forOfDeclarationList.declarations; + + const updatedDeclaration = factory.updateVariableDeclaration( + forOfDeclaration, + forOfDeclaration.name, + forOfDeclaration.exclamationToken, + forOfDeclaration.type, + itemAccessExpression, + ); + + return factory.createVariableStatement( + undefined, // modifiers + factory.updateVariableDeclarationList(forOfDeclarationList, [ + updatedDeclaration, + ]), + ); + } + } + }; + + return transformer; +} diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 7fbb703909..09ab15995e 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -189,7 +189,7 @@ export function visit( let inArray = Array.isArray(root); let keys: any = [root]; let index = -1; - let edits = []; + let edits: Array = []; let node: any = root; let key: any = undefined; let parent: any = undefined; From fb887db494733785141c3e27707ec22f25acb88e Mon Sep 17 00:00:00 2001 From: jdecroock Date: Mon, 30 Sep 2024 09:46:27 +0200 Subject: [PATCH 2/2] Try out array destructuring --- .../introspectionFromSchema-benchmark.js | 17 -- resources/build-npm.ts | 3 +- resources/optimize-array-destructuring.ts | 36 +++ resources/optimize-for-of.ts | 261 +++++++++++++++++- 4 files changed, 297 insertions(+), 20 deletions(-) delete mode 100644 benchmark/introspectionFromSchema-benchmark.js create mode 100644 resources/optimize-array-destructuring.ts diff --git a/benchmark/introspectionFromSchema-benchmark.js b/benchmark/introspectionFromSchema-benchmark.js deleted file mode 100644 index 7e77b36f19..0000000000 --- a/benchmark/introspectionFromSchema-benchmark.js +++ /dev/null @@ -1,17 +0,0 @@ -import { executeSync } from 'graphql/execution/execute.js'; -import { parse } from 'graphql/language/parser.js'; -import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; -import { getIntrospectionQuery } from 'graphql/utilities/getIntrospectionQuery.js'; - -import { bigSchemaSDL } from './fixtures.js'; - -const schema = buildSchema(bigSchemaSDL, { assumeValid: true }); -const document = parse(getIntrospectionQuery()); - -export const benchmark = { - name: 'Execute Introspection Query', - count: 20, - measure() { - executeSync({ schema, document }); - }, -}; diff --git a/resources/build-npm.ts b/resources/build-npm.ts index d75477364d..813b0d7b7f 100644 --- a/resources/build-npm.ts +++ b/resources/build-npm.ts @@ -6,6 +6,7 @@ import ts from 'typescript'; import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; import { inlineInvariant } from './inline-invariant.js'; +import { optimizeArrayDestructuring } from './optimize-array-destructuring.js'; import { optimizeForOf } from './optimize-for-of.js'; import { prettify, @@ -146,7 +147,7 @@ function emitTSFiles(options: { const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost); const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, { - before: [optimizeForOf(tsProgram)], + before: [optimizeForOf(tsProgram), optimizeArrayDestructuring()], after: [changeExtensionInImportPaths({ extension }), inlineInvariant], }); assert( diff --git a/resources/optimize-array-destructuring.ts b/resources/optimize-array-destructuring.ts new file mode 100644 index 0000000000..06f03b8eda --- /dev/null +++ b/resources/optimize-array-destructuring.ts @@ -0,0 +1,36 @@ +import ts from 'typescript'; + +export function optimizeArrayDestructuring() { + const transformer: ts.TransformerFactory = (context) => { + const { factory } = context; + + return (sourceFile: ts.SourceFile): ts.SourceFile => { + const visitor = (node: ts.Node): ts.Node => { + if (ts.isArrayBindingPattern(node)) { + const elements = node.elements + .map((el, i) => { + if (!el.getText()) { + return undefined; + } + return { key: String(i), name: el.getText() }; + }) + .filter(Boolean) as Array<{ key: string; name: string }>; + + const objectBindingPattern = factory.createObjectBindingPattern( + elements.map((el) => { + const key = factory.createIdentifier(el.key); + const name = factory.createIdentifier(el.name); + return factory.createBindingElement(undefined, key, name); + }), + ); + return objectBindingPattern; + } + + return ts.visitEachChild(node, visitor, context); + }; + return ts.visitNode(sourceFile, visitor) as ts.SourceFile; + }; + }; + + return transformer; +} diff --git a/resources/optimize-for-of.ts b/resources/optimize-for-of.ts index c22354d3e1..c2213d7a88 100644 --- a/resources/optimize-for-of.ts +++ b/resources/optimize-for-of.ts @@ -37,12 +37,85 @@ export function optimizeForOf(program: ts.Program) { return ts.visitNode(sourceFile, visitNode) as ts.SourceFile; function visitNode(node: ts.Node): ts.Node { + // TODO: add Set support + // TODO: potentially add support for for..in statements as well if (isArrayForOfStatement(node)) { return convertForOfStatementForArray(node); + } else if (isMapForOfStatement(node)) { + return convertForOfStatementForMap(node); + } else if (isSetForOfStatement(node)) { + return convertForOfStatementForSet(node); } + return ts.visitEachChild(node, visitNode, context); } + function isSetForOfStatement(node: ts.Node): node is ts.ForOfStatement { + if (!ts.isForOfStatement(node) || node.awaitModifier != null) { + return false; + } + + const { expression } = node; + + const expressionType = typeChecker.getTypeAtLocation(expression); + + for (const subType of unionTypeParts(expressionType)) { + assert( + !(subType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)), + 'Can not use any or unknown values in for-of loop: ' + + nodeLocationString(node), + ); + + if (subType.flags & ts.TypeFlags.StringLike) { + continue; + } + + const typeName = subType.getSymbol()?.getName(); + assert(typeName != null); + + if (typeName === 'Set') { + continue; + } + + return false; + } + + return true; + } + + function isMapForOfStatement(node: ts.Node): node is ts.ForOfStatement { + if (!ts.isForOfStatement(node) || node.awaitModifier != null) { + return false; + } + + const { expression } = node; + + const expressionType = typeChecker.getTypeAtLocation(expression); + + for (const subType of unionTypeParts(expressionType)) { + assert( + !(subType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)), + 'Can not use any or unknown values in for-of loop: ' + + nodeLocationString(node), + ); + + if (subType.flags & ts.TypeFlags.StringLike) { + continue; + } + + const typeName = subType.getSymbol()?.getName(); + assert(typeName != null); + + if (typeName === 'Map') { + continue; + } + + return false; + } + + return true; + } + function isArrayForOfStatement(node: ts.Node): node is ts.ForOfStatement { if (!ts.isForOfStatement(node) || node.awaitModifier != null) { return false; @@ -91,6 +164,190 @@ export function optimizeForOf(program: ts.Program) { return (type.flags & ts.TypeFlags.Union) !== 0; } + function convertForOfStatementForSet( + forOfNode: ts.ForOfStatement, + ): ts.Statement { + const counter = factory.createLoopVariable(); + const forDeclarations = [ + factory.createVariableDeclaration( + counter, + undefined, // exclamationToken + undefined, // type + factory.createNumericLiteral(0), + ), + ]; + + // In the case where the user wrote an identifier as the RHS, like this: + // + // for (let v of arr) { } + // + // we don't want to emit a temporary variable for the RHS, just use it directly. + const variableDeclarations = []; + let reference: ts.Identifier = factory.createIdentifier('temp'); + if (ts.isIdentifier(forOfNode.expression)) { + reference = forOfNode.expression; + } else { + const temp = factory.createTempVariable( + (identifier) => (reference = identifier), // recordTempVariable + ); + variableDeclarations.push( + factory.createVariableDeclaration( + temp, + undefined, // exclamationToken + undefined, // type + forOfNode.expression, + ), + ); + } + + const tempText = `_${reference.text}`; + const valuesExpression = factory.createCallExpression( + factory.createPropertyAccessExpression( + reference, + factory.createIdentifier('values'), + ), + undefined, + [], + ); + variableDeclarations.push( + factory.createVariableDeclaration( + tempText, + undefined, + undefined, + valuesExpression, + ), + ); + const rhsReference = factory.createIdentifier(tempText); + + const forInitializer = factory.createVariableDeclarationList( + forDeclarations, + ts.NodeFlags.Let, + ); + + const forCondition = factory.createLessThan( + counter, + factory.createPropertyAccessExpression(rhsReference, 'length'), + ); + const forIncrementor = factory.createPostfixIncrement(counter); + + assert(ts.isVariableDeclarationList(forOfNode.initializer)); + // It will use rhsIterationValue _a[_i] as the initializer. + const itemAssignment = convertForOfInitializer( + forOfNode.initializer, + factory.createElementAccessExpression(rhsReference, counter), + ); + + assert(ts.isBlock(forOfNode.statement)); + const forBody = factory.updateBlock(forOfNode.statement, [ + itemAssignment, + ...forOfNode.statement.statements, + ]); + + const forStatement = factory.createForStatement( + forInitializer, + forCondition, + forIncrementor, + forBody, + ); + + return factory.createBlock([ + factory.createVariableStatement(undefined, variableDeclarations), + forStatement, + ]); + } + + function convertForOfStatementForMap( + forOfNode: ts.ForOfStatement, + ): ts.Statement { + const counter = factory.createLoopVariable(); + const forDeclarations = [ + factory.createVariableDeclaration( + counter, + undefined, // exclamationToken + undefined, // type + factory.createNumericLiteral(0), + ), + ]; + + // In the case where the user wrote an identifier as the RHS, like this: + // + // for (let v of arr) { } + // + // we don't want to emit a temporary variable for the RHS, just use it directly. + const variableDeclarations = []; + let reference: ts.Identifier = factory.createIdentifier('temp'); + if (ts.isIdentifier(forOfNode.expression)) { + reference = forOfNode.expression; + } else { + const temp = factory.createTempVariable( + (identifier) => (reference = identifier), // recordTempVariable + ); + variableDeclarations.push( + factory.createVariableDeclaration( + temp, + undefined, // exclamationToken + undefined, // type + forOfNode.expression, + ), + ); + } + + const tempText = `_${reference.text}`; + const valuesExpression = factory.createCallExpression( + factory.createPropertyAccessExpression( + reference, + factory.createIdentifier('values'), + ), + undefined, + [], + ); + variableDeclarations.push( + factory.createVariableDeclaration( + tempText, + undefined, + undefined, + valuesExpression, + ), + ); + const rhsReference = factory.createIdentifier(tempText); + + const forInitializer = factory.createVariableDeclarationList( + forDeclarations, + ts.NodeFlags.Let, + ); + + const forCondition = factory.createLessThan( + counter, + factory.createPropertyAccessExpression(rhsReference, 'length'), + ); + const forIncrementor = factory.createPostfixIncrement(counter); + + assert(ts.isVariableDeclarationList(forOfNode.initializer)); + // It will use rhsIterationValue _a[_i] as the initializer. + const itemAssignment = convertForOfInitializer( + forOfNode.initializer, + factory.createElementAccessExpression(rhsReference, counter), + ); + + assert(ts.isBlock(forOfNode.statement)); + const forBody = factory.updateBlock(forOfNode.statement, [ + itemAssignment, + ...forOfNode.statement.statements, + ]); + + const forStatement = factory.createForStatement( + forInitializer, + forCondition, + forIncrementor, + forBody, + ); + + return factory.createBlock([ + factory.createVariableStatement(undefined, variableDeclarations), + forStatement, + ]); + } + function convertForOfStatementForArray( forOfNode: ts.ForOfStatement, ): ts.Statement { @@ -126,7 +383,7 @@ export function optimizeForOf(program: ts.Program) { ); } - const forInitiliazer = factory.createVariableDeclarationList( + const forInitializer = factory.createVariableDeclarationList( forDeclarations, ts.NodeFlags.Let, ); @@ -151,7 +408,7 @@ export function optimizeForOf(program: ts.Program) { ]); return factory.createForStatement( - forInitiliazer, + forInitializer, forCondition, forIncrementor, forBody,