Skip to content

Improve __spreadArray perf, and other fixes related to SpreadElement #44527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40726,6 +40726,11 @@ namespace ts {
error(location, Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name, 5);
}
}
else if (helper & ExternalEmitHelpers.SpreadArray) {
if (!some(getSignaturesOfSymbol(symbol), signature => getParameterCount(signature) > 2)) {
error(location, Diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name, 3);
}
}
}
}
}
Expand Down
25 changes: 16 additions & 9 deletions src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace ts {
// ES2015 Helpers
createExtendsHelper(name: Identifier): Expression;
createTemplateObjectHelper(cooked: ArrayLiteralExpression, raw: ArrayLiteralExpression): Expression;
createSpreadArrayHelper(to: Expression, from: Expression): Expression;
createSpreadArrayHelper(to: Expression, from: Expression, packFrom: boolean): Expression;
// ES2015 Destructuring Helpers
createValuesHelper(expression: Expression): Expression;
createReadHelper(iteratorRecord: Expression, count: number | undefined): Expression;
Expand All @@ -38,6 +38,9 @@ namespace ts {

export function createEmitHelperFactory(context: TransformationContext): EmitHelperFactory {
const factory = context.factory;
const immutableTrue = memoize(() => setEmitFlags(factory.createTrue(), EmitFlags.Immutable));
const immutableFalse = memoize(() => setEmitFlags(factory.createFalse(), EmitFlags.Immutable));

return {
getUnscopedHelperName,
// TypeScript Helpers
Expand Down Expand Up @@ -282,12 +285,12 @@ namespace ts {
);
}

function createSpreadArrayHelper(to: Expression, from: Expression) {
function createSpreadArrayHelper(to: Expression, from: Expression, packFrom: boolean) {
context.requestEmitHelper(spreadArrayHelper);
return factory.createCallExpression(
getUnscopedHelperName("__spreadArray"),
/*typeArguments*/ undefined,
[to, from]
[to, from, packFrom ? immutableTrue() : immutableFalse()]
);
}

Expand Down Expand Up @@ -637,10 +640,14 @@ namespace ts {
importName: "__spreadArray",
scoped: false,
text: `
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || from);
};`
};

Expand Down Expand Up @@ -1001,10 +1008,10 @@ namespace ts {
})(name => super[name], (name, value) => super[name] = value);`
};

export function isCallToHelper(firstSegment: Expression, helperName: __String) {
export function isCallToHelper(firstSegment: Expression, helperName: __String): boolean {
return isCallExpression(firstSegment)
&& isIdentifier(firstSegment.expression)
&& (getEmitFlags(firstSegment.expression) & EmitFlags.HelperName)
&& (getEmitFlags(firstSegment.expression) & EmitFlags.HelperName) !== 0
&& firstSegment.expression.escapedText === helperName;
}
}
4 changes: 3 additions & 1 deletion src/compiler/factory/emitNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ namespace ts {

node.emitNode = {} as EmitNode;
}

else {
Debug.assert(!(node.emitNode.flags & EmitFlags.Immutable), "Invalid attempt to mutate an immutable node.");
}
return node.emitNode;
}

Expand Down
32 changes: 25 additions & 7 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,25 @@ namespace ts {
elements = [];
}
else if (isNodeArray(elements)) {
// Ensure the transform flags have been aggregated for this NodeArray
if (elements.transformFlags === undefined) {
aggregateChildrenFlags(elements as MutableNodeArray<T>);
if (hasTrailingComma === undefined || elements.hasTrailingComma === hasTrailingComma) {
// Ensure the transform flags have been aggregated for this NodeArray
if (elements.transformFlags === undefined) {
aggregateChildrenFlags(elements as MutableNodeArray<T>);
}
Debug.attachNodeArrayDebugInfo(elements);
return elements;
}
Debug.attachNodeArrayDebugInfo(elements);
return elements;

// This *was* a `NodeArray`, but the `hasTrailingComma` option differs. Recreate the
// array with the same elements, text range, and transform flags but with the updated
// value for `hasTrailingComma`
const array = elements.slice() as MutableNodeArray<T>;
array.pos = elements.pos;
array.end = elements.end;
array.hasTrailingComma = hasTrailingComma;
array.transformFlags = elements.transformFlags;
Debug.attachNodeArrayDebugInfo(array);
return array;
}

// Since the element list of a node array is typically created by starting with an empty array and
Expand Down Expand Up @@ -2184,7 +2197,12 @@ namespace ts {
// @api
function createArrayLiteralExpression(elements?: readonly Expression[], multiLine?: boolean) {
const node = createBaseExpression<ArrayLiteralExpression>(SyntaxKind.ArrayLiteralExpression);
node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(elements));
// Ensure we add a trailing comma for something like `[NumericLiteral(1), NumericLiteral(2), OmittedExpresion]` so that
// we end up with `[1, 2, ,]` instead of `[1, 2, ]` otherwise the `OmittedExpression` will just end up being treated like
// a trailing comma.
const lastElement = elements && lastOrUndefined(elements);
const elementsArray = createNodeArray(elements, lastElement && isOmittedExpression(lastElement) ? true : undefined);
node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(elementsArray);
node.multiLine = multiLine;
node.transformFlags |= propagateChildrenFlags(node.elements);
return node;
Expand Down Expand Up @@ -6479,7 +6497,7 @@ namespace ts {
// We are using `.slice()` here in case `destEmitNode.leadingComments` is pushed to later.
if (leadingComments) destEmitNode.leadingComments = addRange(leadingComments.slice(), destEmitNode.leadingComments);
if (trailingComments) destEmitNode.trailingComments = addRange(trailingComments.slice(), destEmitNode.trailingComments);
if (flags) destEmitNode.flags = flags;
if (flags) destEmitNode.flags = flags & ~EmitFlags.Immutable;
if (commentRange) destEmitNode.commentRange = commentRange;
if (sourceMapRange) destEmitNode.sourceMapRange = sourceMapRange;
if (tokenSourceMapRanges) destEmitNode.tokenSourceMapRanges = mergeTokenSourceMapRanges(tokenSourceMapRanges, destEmitNode.tokenSourceMapRanges!);
Expand Down
91 changes: 60 additions & 31 deletions src/compiler/transformers/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,21 @@ namespace ts {
FunctionSubtreeExcludes = NewTarget | CapturedLexicalThis,
}

const enum SpreadSegmentKind {
None, // Not a spread segment
UnpackedSpread, // A spread segment that must be packed (i.e., converting `[...[1, , 2]]` into `[1, undefined, 2]`)
PackedSpread, // A spread segment that is known to already be packed (i.e., `[...[1, 2]]` or `[...__read(a)]`)
}

interface SpreadSegment {
kind: SpreadSegmentKind;
expression: Expression;
}

function createSpreadSegment(kind: SpreadSegmentKind, expression: Expression): SpreadSegment {
return { kind, expression };
}

export function transformES2015(context: TransformationContext) {
const {
factory,
Expand Down Expand Up @@ -3599,7 +3614,7 @@ namespace ts {
function visitArrayLiteralExpression(node: ArrayLiteralExpression): Expression {
if (some(node.elements, isSpreadElement)) {
// We are here because we contain a SpreadElementExpression.
return transformAndSpreadElements(node.elements, /*needsUniqueCopy*/ true, !!node.multiLine, /*hasTrailingComma*/ !!node.elements.hasTrailingComma);
return transformAndSpreadElements(node.elements, /*isArgumentList*/ false, !!node.multiLine, /*hasTrailingComma*/ !!node.elements.hasTrailingComma);
}
return visitEachChild(node, visitor, context);
}
Expand Down Expand Up @@ -3820,7 +3835,7 @@ namespace ts {
resultingCall = factory.createFunctionApplyCall(
visitNode(target, callExpressionVisitor, isExpression),
node.expression.kind === SyntaxKind.SuperKeyword ? thisArg : visitNode(thisArg, visitor, isExpression),
transformAndSpreadElements(node.arguments, /*needsUniqueCopy*/ false, /*multiLine*/ false, /*hasTrailingComma*/ false)
transformAndSpreadElements(node.arguments, /*isArgumentList*/ true, /*multiLine*/ false, /*hasTrailingComma*/ false)
);
}
else {
Expand Down Expand Up @@ -3878,7 +3893,7 @@ namespace ts {
factory.createFunctionApplyCall(
visitNode(target, visitor, isExpression),
thisArg,
transformAndSpreadElements(factory.createNodeArray([factory.createVoidZero(), ...node.arguments!]), /*needsUniqueCopy*/ false, /*multiLine*/ false, /*hasTrailingComma*/ false)
transformAndSpreadElements(factory.createNodeArray([factory.createVoidZero(), ...node.arguments!]), /*isArgumentList*/ true, /*multiLine*/ false, /*hasTrailingComma*/ false)
),
/*typeArguments*/ undefined,
[]
Expand All @@ -3891,12 +3906,12 @@ namespace ts {
* Transforms an array of Expression nodes that contains a SpreadExpression.
*
* @param elements The array of Expression nodes.
* @param needsUniqueCopy A value indicating whether to ensure that the result is a fresh array.
* This should be `true` when spreading into an `ArrayLiteral`, and `false` when spreading into an
* @param isArgumentList A value indicating whether to ensure that the result is a fresh array.
* This should be `false` when spreading into an `ArrayLiteral`, and `true` when spreading into an
* argument list.
* @param multiLine A value indicating whether the result should be emitted on multiple lines.
*/
function transformAndSpreadElements(elements: NodeArray<Expression>, needsUniqueCopy: boolean, multiLine: boolean, hasTrailingComma: boolean): Expression {
function transformAndSpreadElements(elements: NodeArray<Expression>, isArgumentList: boolean, multiLine: boolean, hasTrailingComma: boolean): Expression {
// When there is no leading SpreadElement:
//
// [source]
Expand Down Expand Up @@ -3931,7 +3946,10 @@ namespace ts {
// Map spans of spread expressions into their expressions and spans of other
// expressions into an array literal.
const numElements = elements.length;
const segments = flatten<Expression>(
const segments = flatten<SpreadSegment>(
// As we visit each element, we return one of two functions to use as the "key":
// - `visitSpanOfSpreads` for one or more contiguous `...` spread expressions, i.e. `...a, ...b` in `[1, 2, ...a, ...b]`
// - `visitSpanOfNonSpreads` for one or more contiguous non-spread elements, i.e. `1, 2`, in `[1, 2, ...a, ...b]`
spanMap(elements, partitionSpread, (partition, visitPartition, _start, end) =>
visitPartition(partition, multiLine, hasTrailingComma && end === numElements)
)
Expand All @@ -3943,26 +3961,26 @@ namespace ts {
// a CallExpression or NewExpression. When using `--downlevelIteration`, we need
// to coerce this into an array for use with `apply`, so we will use the code path
// that follows instead.
if (!needsUniqueCopy && !compilerOptions.downlevelIteration
|| isPackedArrayLiteral(firstSegment) // see NOTE (above)
|| isCallToHelper(firstSegment, "___spreadArray" as __String)) {
return segments[0];
if (isArgumentList && !compilerOptions.downlevelIteration
|| isPackedArrayLiteral(firstSegment.expression) // see NOTE (above)
|| isCallToHelper(firstSegment.expression, "___spreadArray" as __String)) {
return firstSegment.expression;
}
}

const helpers = emitHelpers();
const startsWithSpread = isSpreadElement(elements[0]);
const startsWithSpread = segments[0].kind !== SpreadSegmentKind.None;
let expression: Expression =
startsWithSpread ? factory.createArrayLiteralExpression() :
segments[0];
segments[0].expression;
for (let i = startsWithSpread ? 0 : 1; i < segments.length; i++) {
const segment = segments[i];
// If this is for an argument list, it doesn't matter if the array is packed or sparse
expression = helpers.createSpreadArrayHelper(
expression,
compilerOptions.downlevelIteration && !isPackedArrayLiteral(segments[i]) ? // see NOTE (above)
helpers.createReadHelper(segments[i], /*count*/ undefined) :
segments[i]);
segment.expression,
segment.kind === SpreadSegmentKind.UnpackedSpread && !isArgumentList);
}

return expression;
}

Expand All @@ -3972,27 +3990,38 @@ namespace ts {
: visitSpanOfNonSpreads;
}

function visitSpanOfSpreads(chunk: Expression[]): VisitResult<Expression> {
function visitSpanOfSpreads(chunk: Expression[]): SpreadSegment[] {
return map(chunk, visitExpressionOfSpread);
}

function visitSpanOfNonSpreads(chunk: Expression[], multiLine: boolean, hasTrailingComma: boolean): VisitResult<Expression> {
return factory.createArrayLiteralExpression(
visitNodes(factory.createNodeArray(chunk, hasTrailingComma), visitor, isExpression),
multiLine
);
function visitExpressionOfSpread(node: SpreadElement): SpreadSegment {
let expression = visitNode(node.expression, visitor, isExpression);

// We don't need to pack already packed array literals, or existing calls to the `__read` helper.
const isCallToReadHelper = isCallToHelper(expression, "___read" as __String);
let kind = isCallToReadHelper || isPackedArrayLiteral(expression) ? SpreadSegmentKind.PackedSpread : SpreadSegmentKind.UnpackedSpread;

// We don't need the `__read` helper for array literals. Array packing will be performed by `__spreadArray`.
if (compilerOptions.downlevelIteration && kind === SpreadSegmentKind.UnpackedSpread && !isArrayLiteralExpression(expression) && !isCallToReadHelper) {
expression = emitHelpers().createReadHelper(expression, /*count*/ undefined);
// the `__read` helper returns a packed array, so we don't need to ensure a packed array
kind = SpreadSegmentKind.PackedSpread;
}

return createSpreadSegment(kind, expression);
}

function visitSpreadElement(node: SpreadElement) {
return visitNode(node.expression, visitor, isExpression);
function visitSpanOfNonSpreads(chunk: Expression[], multiLine: boolean, hasTrailingComma: boolean): SpreadSegment {
const expression = factory.createArrayLiteralExpression(
visitNodes(factory.createNodeArray(chunk, hasTrailingComma), visitor, isExpression),
multiLine);

// We do not pack non-spread segments, this is so that `[1, , ...[2, , 3], , 4]` is properly downleveled to
// `[1, , 2, undefined, 3, , 4]`. See the NOTE in `transformAndSpreadElements`
return createSpreadSegment(SpreadSegmentKind.None, expression);
}

/**
* Transforms the expression of a SpreadExpression node.
*
* @param node A SpreadExpression node.
*/
function visitExpressionOfSpread(node: SpreadElement) {
function visitSpreadElement(node: SpreadElement) {
return visitNode(node.expression, visitor, isExpression);
}

Expand Down
8 changes: 6 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,10 +999,13 @@ namespace ts {
;

/* @internal */
export type MutableNodeArray<T extends Node> = NodeArray<T> & T[];
export interface MutableNodeArray<T extends Node> extends Array<T>, TextRange {
hasTrailingComma: boolean;
/* @internal */ transformFlags: TransformFlags; // Flags for transforms, possibly undefined
}

export interface NodeArray<T extends Node> extends ReadonlyArray<T>, ReadonlyTextRange {
hasTrailingComma?: boolean;
readonly hasTrailingComma: boolean;
/* @internal */ transformFlags: TransformFlags; // Flags for transforms, possibly undefined
}

Expand Down Expand Up @@ -6724,6 +6727,7 @@ namespace ts {
/*@internal*/ TypeScriptClassWrapper = 1 << 25, // The node is an IIFE class wrapper created by the ts transform.
/*@internal*/ NeverApplyImportHelper = 1 << 26, // Indicates the node should never be wrapped with an import star helper (because, for example, it imports tslib itself)
/*@internal*/ IgnoreSourceNewlines = 1 << 27, // Overrides `printerOptions.preserveSourceNewlines` to print this node (and all descendants) with default whitespace.
/*@internal*/ Immutable = 1 << 28, // Indicates a node is a singleton intended to be reused in multiple locations. Any attempt to make further changes to the node will result in an error.
}

export interface EmitHelperBase {
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"unittests/config/showConfig.ts",
"unittests/config/tsconfigParsing.ts",
"unittests/config/tsconfigParsingWatchOptions.ts",
"unittests/evaluation/arraySpread.ts",
"unittests/evaluation/asyncArrow.ts",
"unittests/evaluation/asyncGenerator.ts",
"unittests/evaluation/awaiter.ts",
Expand Down
Loading