Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5491ab4

Browse files
feat: improve tree-shaking by propagate const parameter (#5443)
* feat: improve tree-shaking by propagate const parameter * fix: update old tests (for tree-shaking const param) * test: add test for tree-shaking by propagate const parameter * feat&perf: support object param * style: update coverage * test: update tree-shake-literal-parameter * test: update tree-shake top export * refactor: tree-shaking-literal * fix: test indent * perf: remove same object SPJ getObjectEntity is private, so we can't judge if two object are the same * refactor: support iife * test: tree-shake literal iife * fix: args but not callee should not be optimized * refactor: some logic to function base with comment * feat&perf: support implicitly undefined * test: tree-shake literal conditional * feat: integrate with optimizeCache * test: fix * feat: function argument side effect * style: revert export default change since deoptimizePath will detect * feat: support foo(bar);foo(bar); * test: add more side-effect and top-level test * 4.13.2 * test: add export default test * refactor FunctionParameterState and remove initalization * refactor IIFE * feat: support export default anonymous * fix: nested namespace tracking * feat: support define then export default * performance * refactor: UNKNOWN_EXPRESSION * refactor: reduce complexity * fix: export default function foo and foo called from same mod * style: NodeType * style: remove counter * perf: cache onlyfunctioncall result * style&perf: remove args slice * perf: export default variable * perf: export default variable * style: small updates: naming, private... * perf: LogicalExpression deoptimize cache * style: remove a condition which is always true * style: add protected * style: remove a condition which is always true * style: remove a condition * refactor: lazy bind variable * fix: refresh cache if isReassigned change for ParameterVariable * fix: make sure deoptimize give a final state * style: make coverage more happy --------- Co-authored-by: Lukas Taegert-Atkinson <[email protected]> Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent e6e05cd commit 5491ab4

File tree

83 files changed

+981
-49
lines changed

Some content is hidden

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

83 files changed

+981
-49
lines changed

src/ast/nodes/ConditionalExpression.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type MagicString from 'magic-string';
2-
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
2+
import { BLANK } from '../../utils/blank';
33
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
44
import {
55
findFirstOccurrenceOutsideComment,
@@ -46,15 +46,16 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
4646
}
4747

4848
deoptimizeCache(): void {
49+
this.isBranchResolutionAnalysed = false;
50+
const { expressionsToBeDeoptimized } = this;
51+
this.expressionsToBeDeoptimized = [];
52+
for (const expression of expressionsToBeDeoptimized) {
53+
expression.deoptimizeCache();
54+
}
4955
if (this.usedBranch !== null) {
5056
const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent;
5157
this.usedBranch = null;
5258
unusedBranch.deoptimizePath(UNKNOWN_PATH);
53-
const { expressionsToBeDeoptimized } = this;
54-
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
55-
for (const expression of expressionsToBeDeoptimized) {
56-
expression.deoptimizeCache();
57-
}
5859
}
5960
}
6061

@@ -73,9 +74,9 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
7374
recursionTracker: PathTracker,
7475
origin: DeoptimizableEntity
7576
): LiteralValueOrUnknown {
77+
this.expressionsToBeDeoptimized.push(origin);
7678
const usedBranch = this.getUsedBranch();
7779
if (!usedBranch) return UnknownValue;
78-
this.expressionsToBeDeoptimized.push(origin);
7980
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
8081
}
8182

src/ast/nodes/FunctionDeclaration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export default class FunctionDeclaration extends FunctionNode {
1414
}
1515
}
1616

17+
protected onlyFunctionCallUsed(): boolean {
18+
// call super.onlyFunctionCallUsed for export default anonymous function
19+
return this.id?.variable.getOnlyFunctionCallUsed() ?? super.onlyFunctionCallUsed();
20+
}
21+
1722
parseNode(esTreeNode: GenericEsTreeNode): this {
1823
if (esTreeNode.id !== null) {
1924
this.id = new Identifier(this, this.scope.parent as ChildScope).parseNode(

src/ast/nodes/Identifier.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,12 @@ export default class Identifier extends NodeBase implements PatternNode {
6060
}
6161
}
6262

63+
private isReferenceVariable = false;
6364
bind(): void {
6465
if (!this.variable && isReference(this, this.parent as NodeWithFieldDefinition)) {
6566
this.variable = this.scope.findVariable(this.name);
6667
this.variable.addReference(this);
68+
this.isReferenceVariable = true;
6769
}
6870
}
6971

@@ -295,6 +297,9 @@ export default class Identifier extends NodeBase implements PatternNode {
295297
this.variable.consolidateInitializers();
296298
this.scope.context.requestTreeshakingPass();
297299
}
300+
if (this.isReferenceVariable) {
301+
this.variable!.addUsedPlace(this);
302+
}
298303
}
299304

300305
private getVariableRespectingTDZ(): ExpressionEntity | null {

src/ast/nodes/IfStatement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
77
import BlockStatement from './BlockStatement';
88
import type Identifier from './Identifier';
99
import * as NodeType from './NodeType';
10-
import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression';
10+
import { type LiteralValueOrUnknown } from './shared/Expression';
1111
import {
1212
type ExpressionNode,
1313
type GenericEsTreeNode,
@@ -29,7 +29,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE
2929
private testValue: LiteralValueOrUnknown | typeof unset = unset;
3030

3131
deoptimizeCache(): void {
32-
this.testValue = UnknownValue;
32+
this.testValue = unset;
3333
}
3434

3535
hasEffects(context: HasEffectsContext): boolean {

src/ast/nodes/LogicalExpression.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type MagicString from 'magic-string';
2-
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
2+
import { BLANK } from '../../utils/blank';
33
import {
44
findFirstOccurrenceOutsideComment,
55
findNonWhiteSpace,
@@ -57,22 +57,25 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
5757
}
5858

5959
deoptimizeCache(): void {
60-
if (this.usedBranch) {
61-
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
62-
this.usedBranch = null;
63-
unusedBranch.deoptimizePath(UNKNOWN_PATH);
60+
this.isBranchResolutionAnalysed = false;
61+
if (this.expressionsToBeDeoptimized.length > 0) {
6462
const {
6563
scope: { context },
6664
expressionsToBeDeoptimized
6765
} = this;
68-
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
66+
this.expressionsToBeDeoptimized = [];
6967
for (const expression of expressionsToBeDeoptimized) {
7068
expression.deoptimizeCache();
7169
}
7270
// Request another pass because we need to ensure "include" runs again if
7371
// it is rendered
7472
context.requestTreeshakingPass();
7573
}
74+
if (this.usedBranch) {
75+
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
76+
this.usedBranch = null;
77+
unusedBranch.deoptimizePath(UNKNOWN_PATH);
78+
}
7679
}
7780

7881
deoptimizePath(path: ObjectPath): void {
@@ -90,9 +93,9 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
9093
recursionTracker: PathTracker,
9194
origin: DeoptimizableEntity
9295
): LiteralValueOrUnknown {
96+
this.expressionsToBeDeoptimized.push(origin);
9397
const usedBranch = this.getUsedBranch();
9498
if (!usedBranch) return UnknownValue;
95-
this.expressionsToBeDeoptimized.push(origin);
9699
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
97100
}
98101

src/ast/nodes/MemberExpression.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type MagicString from 'magic-string';
22
import type { AstContext } from '../../Module';
33
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
4-
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
4+
import { BLANK } from '../../utils/blank';
55
import { LOGLEVEL_WARN } from '../../utils/logging';
66
import { logIllegalImportReassignment, logMissingExport } from '../../utils/logs';
77
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
@@ -185,7 +185,7 @@ export default class MemberExpression
185185

186186
deoptimizeCache(): void {
187187
const { expressionsToBeDeoptimized, object } = this;
188-
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
188+
this.expressionsToBeDeoptimized = [];
189189
this.propertyKey = UnknownKey;
190190
object.deoptimizePath(UNKNOWN_PATH);
191191
for (const expression of expressionsToBeDeoptimized) {
@@ -396,6 +396,9 @@ export default class MemberExpression
396396
);
397397
this.scope.context.requestTreeshakingPass();
398398
}
399+
if (this.variable) {
400+
this.variable.addUsedPlace(this);
401+
}
399402
}
400403

401404
private applyAssignmentDeoptimization(): void {

src/ast/nodes/UpdateExpression.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export default class UpdateExpression extends NodeBase {
8787
this.argument.deoptimizePath(EMPTY_PATH);
8888
if (this.argument instanceof Identifier) {
8989
const variable = this.scope.findVariable(this.argument.name);
90-
variable.isReassigned = true;
90+
variable.markReassigned();
9191
}
9292
this.scope.context.requestTreeshakingPass();
9393
}

src/ast/nodes/shared/FunctionBase.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,23 @@ import {
99
} from '../../NodeInteractions';
1010
import type ReturnValueScope from '../../scopes/ReturnValueScope';
1111
import type { ObjectPath, PathTracker } from '../../utils/PathTracker';
12-
import { UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker';
12+
import {
13+
EMPTY_PATH,
14+
SHARED_RECURSION_TRACKER,
15+
UNKNOWN_PATH,
16+
UnknownKey
17+
} from '../../utils/PathTracker';
18+
import { UNDEFINED_EXPRESSION } from '../../values';
1319
import type ParameterVariable from '../../variables/ParameterVariable';
20+
import type Variable from '../../variables/Variable';
1421
import BlockStatement from '../BlockStatement';
22+
import type CallExpression from '../CallExpression';
23+
import type ExportDefaultDeclaration from '../ExportDefaultDeclaration';
1524
import Identifier from '../Identifier';
25+
import * as NodeType from '../NodeType';
1626
import RestElement from '../RestElement';
1727
import type SpreadElement from '../SpreadElement';
28+
import type VariableDeclarator from '../VariableDeclarator';
1829
import { Flag, isFlagSet, setFlag } from './BitFlags';
1930
import type { ExpressionEntity, LiteralValueOrUnknown } from './Expression';
2031
import { UNKNOWN_EXPRESSION, UNKNOWN_RETURN_EXPRESSION } from './Expression';
@@ -27,6 +38,13 @@ import {
2738
import type { ObjectEntity } from './ObjectEntity';
2839
import type { PatternNode } from './Pattern';
2940

41+
type InteractionCalledArguments = NodeInteractionCalled['args'];
42+
43+
// This handler does nothing.
44+
// Since we always re-evaluate argument values in a new tree-shaking pass,
45+
// we don't need to get notified if it is deoptimized.
46+
const EMPTY_DEOPTIMIZABLE_HANDLER = { deoptimizeCache() {} };
47+
3048
export default abstract class FunctionBase extends NodeBase {
3149
declare body: BlockStatement | ExpressionNode;
3250
declare params: PatternNode[];
@@ -57,6 +75,107 @@ export default abstract class FunctionBase extends NodeBase {
5775
this.flags = setFlag(this.flags, Flag.generator, value);
5876
}
5977

78+
private knownParameterValues: (ExpressionEntity | undefined)[] = [];
79+
private allArguments: InteractionCalledArguments[] = [];
80+
/**
81+
* update knownParameterValues when a call is made to this function
82+
* @param newArguments arguments of the call
83+
*/
84+
private updateKnownParameterValues(newArguments: InteractionCalledArguments): void {
85+
for (let position = 0; position < this.params.length; position++) {
86+
// only the "this" argument newArguments[0] can be null
87+
// it's possible that some arguments are empty, so the value is undefined
88+
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
89+
const parameter = this.params[position];
90+
// RestElement can be, and can only be, the last parameter
91+
if (parameter instanceof RestElement) {
92+
return;
93+
}
94+
95+
const knownParameterValue = this.knownParameterValues[position];
96+
if (knownParameterValue === undefined) {
97+
this.knownParameterValues[position] = argument;
98+
continue;
99+
}
100+
if (
101+
knownParameterValue === UNKNOWN_EXPRESSION ||
102+
knownParameterValue === argument ||
103+
(knownParameterValue instanceof Identifier &&
104+
argument instanceof Identifier &&
105+
knownParameterValue.variable === argument.variable)
106+
) {
107+
continue;
108+
}
109+
110+
const oldValue = knownParameterValue.getLiteralValueAtPath(
111+
EMPTY_PATH,
112+
SHARED_RECURSION_TRACKER,
113+
EMPTY_DEOPTIMIZABLE_HANDLER
114+
);
115+
const newValue = argument.getLiteralValueAtPath(
116+
EMPTY_PATH,
117+
SHARED_RECURSION_TRACKER,
118+
EMPTY_DEOPTIMIZABLE_HANDLER
119+
);
120+
if (oldValue !== newValue || typeof oldValue === 'symbol') {
121+
this.knownParameterValues[position] = UNKNOWN_EXPRESSION;
122+
} // else both are the same literal, no need to update
123+
}
124+
}
125+
126+
private forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void {
127+
for (let position = 0; position < this.params.length; position++) {
128+
const parameter = this.params[position];
129+
if (parameter instanceof Identifier) {
130+
const ParameterVariable = parameter.variable as ParameterVariable | null;
131+
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
132+
ParameterVariable?.setKnownValue(argument);
133+
}
134+
}
135+
}
136+
137+
/**
138+
* each time tree-shake starts, this method should be called to reoptimize the parameters
139+
* a parameter's state will change at most twice:
140+
* `undefined` (no call is made) -> an expression -> `UnknownArgument`
141+
* we are sure it will converge, and can use state from last iteration
142+
*/
143+
private applyFunctionParameterOptimization() {
144+
if (this.allArguments.length === 0) {
145+
return;
146+
}
147+
148+
if (this.allArguments.length === 1) {
149+
// we are sure what knownParameterValues will be, so skip it and do setKnownValue
150+
this.forwardArgumentsForFunctionCalledOnce(this.allArguments[0]);
151+
return;
152+
}
153+
154+
// reoptimize all arguments, that's why we save them
155+
for (const argumentsList of this.allArguments) {
156+
this.updateKnownParameterValues(argumentsList);
157+
}
158+
for (let position = 0; position < this.params.length; position++) {
159+
const parameter = this.params[position];
160+
// Parameters without default values
161+
if (parameter instanceof Identifier) {
162+
const parameterVariable = parameter.variable as ParameterVariable | null;
163+
// Only the RestElement may be undefined
164+
const knownParameterValue = this.knownParameterValues[position]!;
165+
parameterVariable?.setKnownValue(knownParameterValue);
166+
}
167+
}
168+
}
169+
170+
private deoptimizeFunctionParameters() {
171+
for (const parameter of this.params) {
172+
if (parameter instanceof Identifier) {
173+
const parameterVariable = parameter.variable as ParameterVariable | null;
174+
parameterVariable?.markReassigned();
175+
}
176+
}
177+
}
178+
60179
protected objectEntity: ObjectEntity | null = null;
61180

62181
deoptimizeArgumentsOnInteractionAtPath(
@@ -84,6 +203,7 @@ export default abstract class FunctionBase extends NodeBase {
84203
this.addArgumentToBeDeoptimized(argument);
85204
}
86205
}
206+
this.allArguments.push(args);
87207
} else {
88208
this.getObjectEntity().deoptimizeArgumentsOnInteractionAtPath(
89209
interaction,
@@ -102,6 +222,7 @@ export default abstract class FunctionBase extends NodeBase {
102222
for (const parameterList of this.scope.parameters) {
103223
for (const parameter of parameterList) {
104224
parameter.deoptimizePath(UNKNOWN_PATH);
225+
parameter.markReassigned();
105226
}
106227
}
107228
}
@@ -180,7 +301,33 @@ export default abstract class FunctionBase extends NodeBase {
180301
return false;
181302
}
182303

304+
/**
305+
* If the function (expression or declaration) is only used as function calls
306+
*/
307+
protected onlyFunctionCallUsed(): boolean {
308+
let variable: Variable | null = null;
309+
if (this.parent.type === NodeType.VariableDeclarator) {
310+
variable = (this.parent as VariableDeclarator).id.variable ?? null;
311+
}
312+
if (this.parent.type === NodeType.ExportDefaultDeclaration) {
313+
variable = (this.parent as ExportDefaultDeclaration).variable;
314+
}
315+
return variable?.getOnlyFunctionCallUsed() ?? false;
316+
}
317+
318+
private functionParametersOptimized = false;
183319
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
320+
const isIIFE =
321+
this.parent.type === NodeType.CallExpression &&
322+
(this.parent as CallExpression).callee === this;
323+
const shoulOptimizeFunctionParameters = isIIFE || this.onlyFunctionCallUsed();
324+
if (shoulOptimizeFunctionParameters) {
325+
this.applyFunctionParameterOptimization();
326+
} else if (this.functionParametersOptimized) {
327+
this.deoptimizeFunctionParameters();
328+
}
329+
this.functionParametersOptimized = shoulOptimizeFunctionParameters;
330+
184331
if (!this.deoptimized) this.applyDeoptimizations();
185332
this.included = true;
186333
const { brokenFlow } = context;

src/ast/variables/ExportDefaultVariable.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ClassDeclaration from '../nodes/ClassDeclaration';
33
import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration';
44
import FunctionDeclaration from '../nodes/FunctionDeclaration';
55
import Identifier, { type IdentifierWithVariable } from '../nodes/Identifier';
6+
import type { NodeBase } from '../nodes/shared/Node';
67
import LocalVariable from './LocalVariable';
78
import UndefinedVariable from './UndefinedVariable';
89
import type Variable from './Variable';
@@ -37,6 +38,15 @@ export default class ExportDefaultVariable extends LocalVariable {
3738
}
3839
}
3940

41+
addUsedPlace(usedPlace: NodeBase): void {
42+
const original = this.getOriginalVariable();
43+
if (original === this) {
44+
super.addUsedPlace(usedPlace);
45+
} else {
46+
original.addUsedPlace(usedPlace);
47+
}
48+
}
49+
4050
forbidName(name: string) {
4151
const original = this.getOriginalVariable();
4252
if (original === this) {

0 commit comments

Comments
 (0)