Skip to content

Commit ad979d0

Browse files
committed
Improve __spreadArray perf, fix array packing and trailing omitted expressions
1 parent 4903c64 commit ad979d0

File tree

60 files changed

+3771
-3305
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3771
-3305
lines changed

src/compiler/factory/emitHelpers.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace ts {
1919
// ES2015 Helpers
2020
createExtendsHelper(name: Identifier): Expression;
2121
createTemplateObjectHelper(cooked: ArrayLiteralExpression, raw: ArrayLiteralExpression): Expression;
22-
createSpreadArrayHelper(to: Expression, from: Expression): Expression;
22+
createSpreadArrayHelper(to: Expression, from: Expression, packFrom: boolean): Expression;
2323
// ES2015 Destructuring Helpers
2424
createValuesHelper(expression: Expression): Expression;
2525
createReadHelper(iteratorRecord: Expression, count: number | undefined): Expression;
@@ -282,12 +282,12 @@ namespace ts {
282282
);
283283
}
284284

285-
function createSpreadArrayHelper(to: Expression, from: Expression) {
285+
function createSpreadArrayHelper(to: Expression, from: Expression, packFrom: boolean) {
286286
context.requestEmitHelper(spreadArrayHelper);
287287
return factory.createCallExpression(
288288
getUnscopedHelperName("__spreadArray"),
289289
/*typeArguments*/ undefined,
290-
[to, from]
290+
packFrom ? [to, from, factory.createTrue()] : [to, from]
291291
);
292292
}
293293

@@ -637,10 +637,14 @@ namespace ts {
637637
importName: "__spreadArray",
638638
scoped: false,
639639
text: `
640-
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
641-
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
642-
to[j] = from[i];
643-
return to;
640+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
641+
if (pack) for (var i = 0, l = from.length, ar; i < l; i++) {
642+
if (ar || !(i in from)) {
643+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
644+
ar[i] = from[i];
645+
}
646+
}
647+
return to.concat(ar || from);
644648
};`
645649
};
646650

@@ -1001,10 +1005,10 @@ namespace ts {
10011005
})(name => super[name], (name, value) => super[name] = value);`
10021006
};
10031007

1004-
export function isCallToHelper(firstSegment: Expression, helperName: __String) {
1008+
export function isCallToHelper(firstSegment: Expression, helperName: __String): boolean {
10051009
return isCallExpression(firstSegment)
10061010
&& isIdentifier(firstSegment.expression)
1007-
&& (getEmitFlags(firstSegment.expression) & EmitFlags.HelperName)
1011+
&& (getEmitFlags(firstSegment.expression) & EmitFlags.HelperName) !== 0
10081012
&& firstSegment.expression.escapedText === helperName;
10091013
}
10101014
}

src/compiler/factory/nodeFactory.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -525,12 +525,25 @@ namespace ts {
525525
elements = [];
526526
}
527527
else if (isNodeArray(elements)) {
528-
// Ensure the transform flags have been aggregated for this NodeArray
529-
if (elements.transformFlags === undefined) {
530-
aggregateChildrenFlags(elements as MutableNodeArray<T>);
528+
if (hasTrailingComma === undefined || elements.hasTrailingComma === hasTrailingComma) {
529+
// Ensure the transform flags have been aggregated for this NodeArray
530+
if (elements.transformFlags === undefined) {
531+
aggregateChildrenFlags(elements as MutableNodeArray<T>);
532+
}
533+
Debug.attachNodeArrayDebugInfo(elements);
534+
return elements;
531535
}
532-
Debug.attachNodeArrayDebugInfo(elements);
533-
return elements;
536+
537+
// This *was* a `NodeArray`, but the `hasTrailingComma` option differs. Recreate the
538+
// array with the same elements, text range, and transform flags but with the updated
539+
// value for `hasTrailingComma`
540+
const array = elements.slice() as MutableNodeArray<T>;
541+
array.pos = elements.pos;
542+
array.end = elements.end;
543+
array.hasTrailingComma = hasTrailingComma;
544+
array.transformFlags = elements.transformFlags;
545+
Debug.attachNodeArrayDebugInfo(array);
546+
return array;
534547
}
535548

536549
// Since the element list of a node array is typically created by starting with an empty array and
@@ -2184,7 +2197,12 @@ namespace ts {
21842197
// @api
21852198
function createArrayLiteralExpression(elements?: readonly Expression[], multiLine?: boolean) {
21862199
const node = createBaseExpression<ArrayLiteralExpression>(SyntaxKind.ArrayLiteralExpression);
2187-
node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(createNodeArray(elements));
2200+
// Ensure we add a trailing comma for something like `[NumericLiteral(1), NumericLiteral(2), OmittedExpresion]` so that
2201+
// we end up with `[1, 2, ,]` instead of `[1, 2, ]` otherwise the `OmittedExpression` will just end up being treated like
2202+
// a trailing comma.
2203+
const lastElement = elements && lastOrUndefined(elements);
2204+
const elementsArray = createNodeArray(elements, lastElement && isOmittedExpression(lastElement) ? true : undefined);
2205+
node.elements = parenthesizerRules().parenthesizeExpressionsOfCommaDelimitedList(elementsArray);
21882206
node.multiLine = multiLine;
21892207
node.transformFlags |= propagateChildrenFlags(node.elements);
21902208
return node;

src/compiler/transformers/es2015.ts

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,21 @@ namespace ts {
242242
FunctionSubtreeExcludes = NewTarget | CapturedLexicalThis,
243243
}
244244

245+
const enum SpreadSegmentKind {
246+
None, // Not a spread segment
247+
UnpackedSpread, // A spread segment that must be packed (i.e., converting `[...[1, , 2]]` into `[1, undefined, 2]`)
248+
PackedSpread, // A spread segment that is known to already be packed (i.e., `[...[1, 2]]` or `[...__read(a)]`)
249+
}
250+
251+
interface SpreadSegment {
252+
kind: SpreadSegmentKind;
253+
expression: Expression;
254+
}
255+
256+
function createSpreadSegment(kind: SpreadSegmentKind, expression: Expression): SpreadSegment {
257+
return { kind, expression };
258+
}
259+
245260
export function transformES2015(context: TransformationContext) {
246261
const {
247262
factory,
@@ -3599,7 +3614,7 @@ namespace ts {
35993614
function visitArrayLiteralExpression(node: ArrayLiteralExpression): Expression {
36003615
if (some(node.elements, isSpreadElement)) {
36013616
// We are here because we contain a SpreadElementExpression.
3602-
return transformAndSpreadElements(node.elements, /*needsUniqueCopy*/ true, !!node.multiLine, /*hasTrailingComma*/ !!node.elements.hasTrailingComma);
3617+
return transformAndSpreadElements(node.elements, /*isArgumentList*/ false, !!node.multiLine, /*hasTrailingComma*/ !!node.elements.hasTrailingComma);
36033618
}
36043619
return visitEachChild(node, visitor, context);
36053620
}
@@ -3820,7 +3835,7 @@ namespace ts {
38203835
resultingCall = factory.createFunctionApplyCall(
38213836
visitNode(target, callExpressionVisitor, isExpression),
38223837
node.expression.kind === SyntaxKind.SuperKeyword ? thisArg : visitNode(thisArg, visitor, isExpression),
3823-
transformAndSpreadElements(node.arguments, /*needsUniqueCopy*/ false, /*multiLine*/ false, /*hasTrailingComma*/ false)
3838+
transformAndSpreadElements(node.arguments, /*isArgumentList*/ true, /*multiLine*/ false, /*hasTrailingComma*/ false)
38243839
);
38253840
}
38263841
else {
@@ -3878,7 +3893,7 @@ namespace ts {
38783893
factory.createFunctionApplyCall(
38793894
visitNode(target, visitor, isExpression),
38803895
thisArg,
3881-
transformAndSpreadElements(factory.createNodeArray([factory.createVoidZero(), ...node.arguments!]), /*needsUniqueCopy*/ false, /*multiLine*/ false, /*hasTrailingComma*/ false)
3896+
transformAndSpreadElements(factory.createNodeArray([factory.createVoidZero(), ...node.arguments!]), /*isArgumentList*/ true, /*multiLine*/ false, /*hasTrailingComma*/ false)
38823897
),
38833898
/*typeArguments*/ undefined,
38843899
[]
@@ -3891,12 +3906,12 @@ namespace ts {
38913906
* Transforms an array of Expression nodes that contains a SpreadExpression.
38923907
*
38933908
* @param elements The array of Expression nodes.
3894-
* @param needsUniqueCopy A value indicating whether to ensure that the result is a fresh array.
3895-
* This should be `true` when spreading into an `ArrayLiteral`, and `false` when spreading into an
3909+
* @param isArgumentList A value indicating whether to ensure that the result is a fresh array.
3910+
* This should be `false` when spreading into an `ArrayLiteral`, and `true` when spreading into an
38963911
* argument list.
38973912
* @param multiLine A value indicating whether the result should be emitted on multiple lines.
38983913
*/
3899-
function transformAndSpreadElements(elements: NodeArray<Expression>, needsUniqueCopy: boolean, multiLine: boolean, hasTrailingComma: boolean): Expression {
3914+
function transformAndSpreadElements(elements: NodeArray<Expression>, isArgumentList: boolean, multiLine: boolean, hasTrailingComma: boolean): Expression {
39003915
// When there is no leading SpreadElement:
39013916
//
39023917
// [source]
@@ -3931,7 +3946,10 @@ namespace ts {
39313946
// Map spans of spread expressions into their expressions and spans of other
39323947
// expressions into an array literal.
39333948
const numElements = elements.length;
3934-
const segments = flatten<Expression>(
3949+
const segments = flatten<SpreadSegment>(
3950+
// As we visit each element, we return one of two functions to use as the "key":
3951+
// - `visitSpanOfSpreads` for one or more contiguous `...` spread expressions, i.e. `...a, ...b` in `[1, 2, ...a, ...b]`
3952+
// - `visitSpanOfNonSpreads` for one or more contiguous non-spread elements, i.e. `1, 2`, in `[1, 2, ...a, ...b]`
39353953
spanMap(elements, partitionSpread, (partition, visitPartition, _start, end) =>
39363954
visitPartition(partition, multiLine, hasTrailingComma && end === numElements)
39373955
)
@@ -3943,26 +3961,26 @@ namespace ts {
39433961
// a CallExpression or NewExpression. When using `--downlevelIteration`, we need
39443962
// to coerce this into an array for use with `apply`, so we will use the code path
39453963
// that follows instead.
3946-
if (!needsUniqueCopy && !compilerOptions.downlevelIteration
3947-
|| isPackedArrayLiteral(firstSegment) // see NOTE (above)
3948-
|| isCallToHelper(firstSegment, "___spreadArray" as __String)) {
3949-
return segments[0];
3964+
if (isArgumentList && !compilerOptions.downlevelIteration
3965+
|| isPackedArrayLiteral(firstSegment.expression) // see NOTE (above)
3966+
|| isCallToHelper(firstSegment.expression, "___spreadArray" as __String)) {
3967+
return firstSegment.expression;
39503968
}
39513969
}
39523970

39533971
const helpers = emitHelpers();
3954-
const startsWithSpread = isSpreadElement(elements[0]);
3972+
const startsWithSpread = segments[0].kind !== SpreadSegmentKind.None;
39553973
let expression: Expression =
39563974
startsWithSpread ? factory.createArrayLiteralExpression() :
3957-
segments[0];
3975+
segments[0].expression;
39583976
for (let i = startsWithSpread ? 0 : 1; i < segments.length; i++) {
3977+
const segment = segments[i];
3978+
// If this is for an argument list, it doesn't matter if the array is packed or sparse
39593979
expression = helpers.createSpreadArrayHelper(
39603980
expression,
3961-
compilerOptions.downlevelIteration && !isPackedArrayLiteral(segments[i]) ? // see NOTE (above)
3962-
helpers.createReadHelper(segments[i], /*count*/ undefined) :
3963-
segments[i]);
3981+
segment.expression,
3982+
segment.kind === SpreadSegmentKind.UnpackedSpread && !isArgumentList);
39643983
}
3965-
39663984
return expression;
39673985
}
39683986

@@ -3972,27 +3990,38 @@ namespace ts {
39723990
: visitSpanOfNonSpreads;
39733991
}
39743992

3975-
function visitSpanOfSpreads(chunk: Expression[]): VisitResult<Expression> {
3993+
function visitSpanOfSpreads(chunk: Expression[]): SpreadSegment[] {
39763994
return map(chunk, visitExpressionOfSpread);
39773995
}
39783996

3979-
function visitSpanOfNonSpreads(chunk: Expression[], multiLine: boolean, hasTrailingComma: boolean): VisitResult<Expression> {
3980-
return factory.createArrayLiteralExpression(
3981-
visitNodes(factory.createNodeArray(chunk, hasTrailingComma), visitor, isExpression),
3982-
multiLine
3983-
);
3997+
function visitExpressionOfSpread(node: SpreadElement): SpreadSegment {
3998+
let expression = visitNode(node.expression, visitor, isExpression);
3999+
4000+
// We don't need to pack already packed array literals, or existing calls to the `__read` helper.
4001+
const isCallToReadHelper = isCallToHelper(expression, "___read" as __String);
4002+
let kind = isCallToReadHelper || isPackedArrayLiteral(expression) ? SpreadSegmentKind.PackedSpread : SpreadSegmentKind.UnpackedSpread;
4003+
4004+
// We don't need the `__read` helper for array literals. Array packing will be performed by `__spreadArray`.
4005+
if (compilerOptions.downlevelIteration && kind === SpreadSegmentKind.UnpackedSpread && !isArrayLiteralExpression(expression) && !isCallToReadHelper) {
4006+
expression = emitHelpers().createReadHelper(expression, /*count*/ undefined);
4007+
// the `__read` helper returns a packed array, so we don't need to ensure a packed array
4008+
kind = SpreadSegmentKind.PackedSpread;
4009+
}
4010+
4011+
return createSpreadSegment(kind, expression);
39844012
}
39854013

3986-
function visitSpreadElement(node: SpreadElement) {
3987-
return visitNode(node.expression, visitor, isExpression);
4014+
function visitSpanOfNonSpreads(chunk: Expression[], multiLine: boolean, hasTrailingComma: boolean): SpreadSegment {
4015+
const expression = factory.createArrayLiteralExpression(
4016+
visitNodes(factory.createNodeArray(chunk, hasTrailingComma), visitor, isExpression),
4017+
multiLine);
4018+
4019+
// We do not pack non-spread segments, this is so that `[1, , ...[2, , 3], , 4]` is properly downleveled to
4020+
// `[1, , 2, undefined, 3, , 4]`. See the NOTE in `transformAndSpreadElements`
4021+
return createSpreadSegment(SpreadSegmentKind.None, expression);
39884022
}
39894023

3990-
/**
3991-
* Transforms the expression of a SpreadExpression node.
3992-
*
3993-
* @param node A SpreadExpression node.
3994-
*/
3995-
function visitExpressionOfSpread(node: SpreadElement) {
4024+
function visitSpreadElement(node: SpreadElement) {
39964025
return visitNode(node.expression, visitor, isExpression);
39974026
}
39984027

src/compiler/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -999,10 +999,13 @@ namespace ts {
999999
;
10001000

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

10041007
export interface NodeArray<T extends Node> extends ReadonlyArray<T>, ReadonlyTextRange {
1005-
hasTrailingComma?: boolean;
1008+
readonly hasTrailingComma: boolean;
10061009
/* @internal */ transformFlags: TransformFlags; // Flags for transforms, possibly undefined
10071010
}
10081011

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"unittests/config/showConfig.ts",
8989
"unittests/config/tsconfigParsing.ts",
9090
"unittests/config/tsconfigParsingWatchOptions.ts",
91+
"unittests/evaluation/arraySpread.ts",
9192
"unittests/evaluation/asyncArrow.ts",
9293
"unittests/evaluation/asyncGenerator.ts",
9394
"unittests/evaluation/awaiter.ts",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
describe("unittests:: evaluation:: arraySpread", () => {
2+
it("array spread preserves side-effects", async () => {
3+
const result = evaluator.evaluateTypeScript(`
4+
const k = [1, 2];
5+
const o = [3, ...k, k[0]++];
6+
export const output = o;
7+
`);
8+
assert.deepEqual(result.output, [3, 1, 2, 1]);
9+
});
10+
it("array spread packs spread elements", async () => {
11+
const result = evaluator.evaluateTypeScript(`
12+
const k = [1, , 2];
13+
const o = [3, ...k, 4];
14+
export const output = o;
15+
`);
16+
assert.deepEqual(result.output, [3, 1, undefined, 2, 4]);
17+
assert.hasAllKeys(result.output, ["0", "1", "2", "3", "4"]);
18+
});
19+
it("array spread does not pack non-spread elements", async () => {
20+
const result = evaluator.evaluateTypeScript(`
21+
const k = [1, 2];
22+
const o = [3, , ...k, , 4];
23+
export const output = o;
24+
`);
25+
assert.deepEqual(result.output, [3, , 1, 2, , 4]); // eslint-disable-line no-sparse-arrays
26+
assert.hasAllKeys(result.output, ["0", "2", "3", "5"]);
27+
assert.doesNotHaveAllKeys(result.output, ["1", "4"]);
28+
});
29+
it("argument spread pack does not matter", async () => {
30+
const result = evaluator.evaluateTypeScript(`
31+
const f = (...args) => args;
32+
const k = [1, , 2];
33+
const o = f(3, ...k, 4);
34+
export const output = o;
35+
`);
36+
assert.deepEqual(result.output, [3, 1, undefined, 2,4]);
37+
assert.hasAllKeys(result.output, ["0", "1", "2", "3", "4"]);
38+
});
39+
});

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ declare namespace ts {
574574
export type HasInitializer = HasExpressionInitializer | ForStatement | ForInStatement | ForOfStatement | JsxAttribute;
575575
export type HasExpressionInitializer = VariableDeclaration | ParameterDeclaration | BindingElement | PropertySignature | PropertyDeclaration | PropertyAssignment | EnumMember;
576576
export interface NodeArray<T extends Node> extends ReadonlyArray<T>, ReadonlyTextRange {
577-
hasTrailingComma?: boolean;
577+
readonly hasTrailingComma: boolean;
578578
}
579579
export interface Token<TKind extends SyntaxKind> extends Node {
580580
readonly kind: TKind;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ declare namespace ts {
574574
export type HasInitializer = HasExpressionInitializer | ForStatement | ForInStatement | ForOfStatement | JsxAttribute;
575575
export type HasExpressionInitializer = VariableDeclaration | ParameterDeclaration | BindingElement | PropertySignature | PropertyDeclaration | PropertyAssignment | EnumMember;
576576
export interface NodeArray<T extends Node> extends ReadonlyArray<T>, ReadonlyTextRange {
577-
hasTrailingComma?: boolean;
577+
readonly hasTrailingComma: boolean;
578578
}
579579
export interface Token<TKind extends SyntaxKind> extends Node {
580580
readonly kind: TKind;

0 commit comments

Comments
 (0)