Skip to content

Commit

Permalink
feat: improve tree-shaking by propagate const parameter (#5443)
Browse files Browse the repository at this point in the history
* 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 <lukas.taegert-atkinson@tngtech.com>
Co-authored-by: Lukas Taegert-Atkinson <lukastaegert@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 21, 2024
1 parent e6e05cd commit 5491ab4
Show file tree
Hide file tree
Showing 83 changed files with 981 additions and 49 deletions.
15 changes: 8 additions & 7 deletions src/ast/nodes/ConditionalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import {
findFirstOccurrenceOutsideComment,
Expand Down Expand Up @@ -46,15 +46,16 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
}

deoptimizeCache(): void {
this.isBranchResolutionAnalysed = false;
const { expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = [];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
if (this.usedBranch !== null) {
const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
const { expressionsToBeDeoptimized } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
}
}

Expand All @@ -73,9 +74,9 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
this.expressionsToBeDeoptimized.push(origin);
const usedBranch = this.getUsedBranch();
if (!usedBranch) return UnknownValue;
this.expressionsToBeDeoptimized.push(origin);
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
}

Expand Down
5 changes: 5 additions & 0 deletions src/ast/nodes/FunctionDeclaration.ts
Expand Up @@ -14,6 +14,11 @@ export default class FunctionDeclaration extends FunctionNode {
}
}

protected onlyFunctionCallUsed(): boolean {
// call super.onlyFunctionCallUsed for export default anonymous function
return this.id?.variable.getOnlyFunctionCallUsed() ?? super.onlyFunctionCallUsed();
}

parseNode(esTreeNode: GenericEsTreeNode): this {
if (esTreeNode.id !== null) {
this.id = new Identifier(this, this.scope.parent as ChildScope).parseNode(
Expand Down
5 changes: 5 additions & 0 deletions src/ast/nodes/Identifier.ts
Expand Up @@ -60,10 +60,12 @@ export default class Identifier extends NodeBase implements PatternNode {
}
}

private isReferenceVariable = false;
bind(): void {
if (!this.variable && isReference(this, this.parent as NodeWithFieldDefinition)) {
this.variable = this.scope.findVariable(this.name);
this.variable.addReference(this);
this.isReferenceVariable = true;
}
}

Expand Down Expand Up @@ -295,6 +297,9 @@ export default class Identifier extends NodeBase implements PatternNode {
this.variable.consolidateInitializers();
this.scope.context.requestTreeshakingPass();
}
if (this.isReferenceVariable) {
this.variable!.addUsedPlace(this);
}
}

private getVariableRespectingTDZ(): ExpressionEntity | null {
Expand Down
4 changes: 2 additions & 2 deletions src/ast/nodes/IfStatement.ts
Expand Up @@ -7,7 +7,7 @@ import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker';
import BlockStatement from './BlockStatement';
import type Identifier from './Identifier';
import * as NodeType from './NodeType';
import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression';
import { type LiteralValueOrUnknown } from './shared/Expression';
import {
type ExpressionNode,
type GenericEsTreeNode,
Expand All @@ -29,7 +29,7 @@ export default class IfStatement extends StatementBase implements DeoptimizableE
private testValue: LiteralValueOrUnknown | typeof unset = unset;

deoptimizeCache(): void {
this.testValue = UnknownValue;
this.testValue = unset;
}

hasEffects(context: HasEffectsContext): boolean {
Expand Down
17 changes: 10 additions & 7 deletions src/ast/nodes/LogicalExpression.ts
@@ -1,5 +1,5 @@
import type MagicString from 'magic-string';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import {
findFirstOccurrenceOutsideComment,
findNonWhiteSpace,
Expand Down Expand Up @@ -57,22 +57,25 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
}

deoptimizeCache(): void {
if (this.usedBranch) {
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
this.isBranchResolutionAnalysed = false;
if (this.expressionsToBeDeoptimized.length > 0) {
const {
scope: { context },
expressionsToBeDeoptimized
} = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
this.expressionsToBeDeoptimized = [];
for (const expression of expressionsToBeDeoptimized) {
expression.deoptimizeCache();
}
// Request another pass because we need to ensure "include" runs again if
// it is rendered
context.requestTreeshakingPass();
}
if (this.usedBranch) {
const unusedBranch = this.usedBranch === this.left ? this.right : this.left;
this.usedBranch = null;
unusedBranch.deoptimizePath(UNKNOWN_PATH);
}
}

deoptimizePath(path: ObjectPath): void {
Expand All @@ -90,9 +93,9 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
this.expressionsToBeDeoptimized.push(origin);
const usedBranch = this.getUsedBranch();
if (!usedBranch) return UnknownValue;
this.expressionsToBeDeoptimized.push(origin);
return usedBranch.getLiteralValueAtPath(path, recursionTracker, origin);
}

Expand Down
7 changes: 5 additions & 2 deletions src/ast/nodes/MemberExpression.ts
@@ -1,7 +1,7 @@
import type MagicString from 'magic-string';
import type { AstContext } from '../../Module';
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
import { BLANK, EMPTY_ARRAY } from '../../utils/blank';
import { BLANK } from '../../utils/blank';
import { LOGLEVEL_WARN } from '../../utils/logging';
import { logIllegalImportReassignment, logMissingExport } from '../../utils/logs';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
Expand Down Expand Up @@ -185,7 +185,7 @@ export default class MemberExpression

deoptimizeCache(): void {
const { expressionsToBeDeoptimized, object } = this;
this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[];
this.expressionsToBeDeoptimized = [];
this.propertyKey = UnknownKey;
object.deoptimizePath(UNKNOWN_PATH);
for (const expression of expressionsToBeDeoptimized) {
Expand Down Expand Up @@ -396,6 +396,9 @@ export default class MemberExpression
);
this.scope.context.requestTreeshakingPass();
}
if (this.variable) {
this.variable.addUsedPlace(this);
}
}

private applyAssignmentDeoptimization(): void {
Expand Down
2 changes: 1 addition & 1 deletion src/ast/nodes/UpdateExpression.ts
Expand Up @@ -87,7 +87,7 @@ export default class UpdateExpression extends NodeBase {
this.argument.deoptimizePath(EMPTY_PATH);
if (this.argument instanceof Identifier) {
const variable = this.scope.findVariable(this.argument.name);
variable.isReassigned = true;
variable.markReassigned();
}
this.scope.context.requestTreeshakingPass();
}
Expand Down
149 changes: 148 additions & 1 deletion src/ast/nodes/shared/FunctionBase.ts
Expand Up @@ -9,12 +9,23 @@ import {
} from '../../NodeInteractions';
import type ReturnValueScope from '../../scopes/ReturnValueScope';
import type { ObjectPath, PathTracker } from '../../utils/PathTracker';
import { UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker';
import {
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
UNKNOWN_PATH,
UnknownKey
} from '../../utils/PathTracker';
import { UNDEFINED_EXPRESSION } from '../../values';
import type ParameterVariable from '../../variables/ParameterVariable';
import type Variable from '../../variables/Variable';
import BlockStatement from '../BlockStatement';
import type CallExpression from '../CallExpression';
import type ExportDefaultDeclaration from '../ExportDefaultDeclaration';
import Identifier from '../Identifier';
import * as NodeType from '../NodeType';
import RestElement from '../RestElement';
import type SpreadElement from '../SpreadElement';
import type VariableDeclarator from '../VariableDeclarator';
import { Flag, isFlagSet, setFlag } from './BitFlags';
import type { ExpressionEntity, LiteralValueOrUnknown } from './Expression';
import { UNKNOWN_EXPRESSION, UNKNOWN_RETURN_EXPRESSION } from './Expression';
Expand All @@ -27,6 +38,13 @@ import {
import type { ObjectEntity } from './ObjectEntity';
import type { PatternNode } from './Pattern';

type InteractionCalledArguments = NodeInteractionCalled['args'];

// This handler does nothing.
// Since we always re-evaluate argument values in a new tree-shaking pass,
// we don't need to get notified if it is deoptimized.
const EMPTY_DEOPTIMIZABLE_HANDLER = { deoptimizeCache() {} };

export default abstract class FunctionBase extends NodeBase {
declare body: BlockStatement | ExpressionNode;
declare params: PatternNode[];
Expand Down Expand Up @@ -57,6 +75,107 @@ export default abstract class FunctionBase extends NodeBase {
this.flags = setFlag(this.flags, Flag.generator, value);
}

private knownParameterValues: (ExpressionEntity | undefined)[] = [];
private allArguments: InteractionCalledArguments[] = [];
/**
* update knownParameterValues when a call is made to this function
* @param newArguments arguments of the call
*/
private updateKnownParameterValues(newArguments: InteractionCalledArguments): void {
for (let position = 0; position < this.params.length; position++) {
// only the "this" argument newArguments[0] can be null
// it's possible that some arguments are empty, so the value is undefined
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
const parameter = this.params[position];
// RestElement can be, and can only be, the last parameter
if (parameter instanceof RestElement) {
return;
}

const knownParameterValue = this.knownParameterValues[position];
if (knownParameterValue === undefined) {
this.knownParameterValues[position] = argument;
continue;
}
if (
knownParameterValue === UNKNOWN_EXPRESSION ||
knownParameterValue === argument ||
(knownParameterValue instanceof Identifier &&
argument instanceof Identifier &&
knownParameterValue.variable === argument.variable)
) {
continue;
}

const oldValue = knownParameterValue.getLiteralValueAtPath(
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
EMPTY_DEOPTIMIZABLE_HANDLER
);
const newValue = argument.getLiteralValueAtPath(
EMPTY_PATH,
SHARED_RECURSION_TRACKER,
EMPTY_DEOPTIMIZABLE_HANDLER
);
if (oldValue !== newValue || typeof oldValue === 'symbol') {
this.knownParameterValues[position] = UNKNOWN_EXPRESSION;
} // else both are the same literal, no need to update
}
}

private forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void {
for (let position = 0; position < this.params.length; position++) {
const parameter = this.params[position];
if (parameter instanceof Identifier) {
const ParameterVariable = parameter.variable as ParameterVariable | null;
const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION;
ParameterVariable?.setKnownValue(argument);
}
}
}

/**
* each time tree-shake starts, this method should be called to reoptimize the parameters
* a parameter's state will change at most twice:
* `undefined` (no call is made) -> an expression -> `UnknownArgument`
* we are sure it will converge, and can use state from last iteration
*/
private applyFunctionParameterOptimization() {
if (this.allArguments.length === 0) {
return;
}

if (this.allArguments.length === 1) {
// we are sure what knownParameterValues will be, so skip it and do setKnownValue
this.forwardArgumentsForFunctionCalledOnce(this.allArguments[0]);
return;
}

// reoptimize all arguments, that's why we save them
for (const argumentsList of this.allArguments) {
this.updateKnownParameterValues(argumentsList);
}
for (let position = 0; position < this.params.length; position++) {
const parameter = this.params[position];
// Parameters without default values
if (parameter instanceof Identifier) {
const parameterVariable = parameter.variable as ParameterVariable | null;
// Only the RestElement may be undefined
const knownParameterValue = this.knownParameterValues[position]!;
parameterVariable?.setKnownValue(knownParameterValue);
}
}
}

private deoptimizeFunctionParameters() {
for (const parameter of this.params) {
if (parameter instanceof Identifier) {
const parameterVariable = parameter.variable as ParameterVariable | null;
parameterVariable?.markReassigned();
}
}
}

protected objectEntity: ObjectEntity | null = null;

deoptimizeArgumentsOnInteractionAtPath(
Expand Down Expand Up @@ -84,6 +203,7 @@ export default abstract class FunctionBase extends NodeBase {
this.addArgumentToBeDeoptimized(argument);
}
}
this.allArguments.push(args);
} else {
this.getObjectEntity().deoptimizeArgumentsOnInteractionAtPath(
interaction,
Expand All @@ -102,6 +222,7 @@ export default abstract class FunctionBase extends NodeBase {
for (const parameterList of this.scope.parameters) {
for (const parameter of parameterList) {
parameter.deoptimizePath(UNKNOWN_PATH);
parameter.markReassigned();
}
}
}
Expand Down Expand Up @@ -180,7 +301,33 @@ export default abstract class FunctionBase extends NodeBase {
return false;
}

/**
* If the function (expression or declaration) is only used as function calls
*/
protected onlyFunctionCallUsed(): boolean {
let variable: Variable | null = null;
if (this.parent.type === NodeType.VariableDeclarator) {
variable = (this.parent as VariableDeclarator).id.variable ?? null;
}
if (this.parent.type === NodeType.ExportDefaultDeclaration) {
variable = (this.parent as ExportDefaultDeclaration).variable;
}
return variable?.getOnlyFunctionCallUsed() ?? false;
}

private functionParametersOptimized = false;
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
const isIIFE =
this.parent.type === NodeType.CallExpression &&
(this.parent as CallExpression).callee === this;
const shoulOptimizeFunctionParameters = isIIFE || this.onlyFunctionCallUsed();
if (shoulOptimizeFunctionParameters) {
this.applyFunctionParameterOptimization();
} else if (this.functionParametersOptimized) {
this.deoptimizeFunctionParameters();
}
this.functionParametersOptimized = shoulOptimizeFunctionParameters;

if (!this.deoptimized) this.applyDeoptimizations();
this.included = true;
const { brokenFlow } = context;
Expand Down
10 changes: 10 additions & 0 deletions src/ast/variables/ExportDefaultVariable.ts
Expand Up @@ -3,6 +3,7 @@ import ClassDeclaration from '../nodes/ClassDeclaration';
import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration';
import FunctionDeclaration from '../nodes/FunctionDeclaration';
import Identifier, { type IdentifierWithVariable } from '../nodes/Identifier';
import type { NodeBase } from '../nodes/shared/Node';
import LocalVariable from './LocalVariable';
import UndefinedVariable from './UndefinedVariable';
import type Variable from './Variable';
Expand Down Expand Up @@ -37,6 +38,15 @@ export default class ExportDefaultVariable extends LocalVariable {
}
}

addUsedPlace(usedPlace: NodeBase): void {
const original = this.getOriginalVariable();
if (original === this) {
super.addUsedPlace(usedPlace);
} else {
original.addUsedPlace(usedPlace);
}
}

forbidName(name: string) {
const original = this.getOriginalVariable();
if (original === this) {
Expand Down

0 comments on commit 5491ab4

Please sign in to comment.