From 58fba0df633f9fadb146c90869623fe9734727b6 Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 24 Mar 2024 00:06:02 +0800 Subject: [PATCH 01/49] feat: improve tree-shaking by propagate const parameter --- src/Graph.ts | 2 + src/ast/nodes/Identifier.ts | 1 + src/ast/nodes/MemberExpression.ts | 3 + src/ast/variables/ExportDefaultVariable.ts | 12 +++ src/ast/variables/ParameterVariable.ts | 23 +++++- src/ast/variables/Variable.ts | 7 ++ src/utils/functionParameterPass.ts | 86 ++++++++++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/utils/functionParameterPass.ts diff --git a/src/Graph.ts b/src/Graph.ts index fc47bfa3c54..b0a66dd4e7f 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -18,6 +18,7 @@ import { PluginDriver } from './utils/PluginDriver'; import Queue from './utils/Queue'; import { BuildPhase } from './utils/buildPhase'; import { analyseModuleExecution } from './utils/executionOrder'; +import { functionParameterPass } from './utils/functionParameterPass'; import { LOGLEVEL_WARN } from './utils/logging'; import { error, @@ -160,6 +161,7 @@ export default class Graph { } private includeStatements(): void { + functionParameterPass(this.modules); const entryModules = [...this.entryModules, ...this.implicitEntryModules]; for (const module of entryModules) { markModuleAndImpureDependenciesAsExecuted(module); diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index b230e7ea580..740c97449bc 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -70,6 +70,7 @@ export default class Identifier extends NodeBase implements PatternNode { if (!this.variable && isReference(this, this.parent as NodeWithFieldDefinition)) { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); + this.variable.addUsedPlace(this); } } diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 5c32a896b3d..f2ba221c147 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -156,6 +156,9 @@ export default class MemberExpression this.isUndefined = true; } else { this.variable = resolvedVariable; + if (this.object instanceof Identifier) { + this.variable.addUsedPlace(this); + } this.scope.addNamespaceMemberAccess(getStringFromPath(path!), resolvedVariable); } } else { diff --git a/src/ast/variables/ExportDefaultVariable.ts b/src/ast/variables/ExportDefaultVariable.ts index 979405b5784..31d1b93d5a2 100644 --- a/src/ast/variables/ExportDefaultVariable.ts +++ b/src/ast/variables/ExportDefaultVariable.ts @@ -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'; @@ -37,6 +38,16 @@ export default class ExportDefaultVariable extends LocalVariable { } } + addUsedPlace(identifier: NodeBase): void { + const original = this.getOriginalVariable(); + this.originalVariable = null; + if (original === this) { + super.addUsedPlace(identifier); + } else { + original.addUsedPlace(identifier); + } + } + forbidName(name: string) { const original = this.getOriginalVariable(); if (original === this) { @@ -57,6 +68,7 @@ export default class ExportDefaultVariable extends LocalVariable { getDirectOriginalVariable(): Variable | null { return this.originalId && + this.originalId.variable && (this.hasId || !( this.originalId.isPossibleTDZ() || diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 94f3feb90d9..ce056aa0abb 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -1,14 +1,17 @@ import type { AstContext } from '../../Module'; import { EMPTY_ARRAY } from '../../utils/blank'; +import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_CALLED } from '../NodeInteractions'; import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; -import type { ExpressionEntity } from '../nodes/shared/Expression'; +import type { LiteralValue } from '../nodes/Literal'; +import type { ExpressionEntity, LiteralValueOrUnknown } from '../nodes/shared/Expression'; import { deoptimizeInteraction, UNKNOWN_EXPRESSION, - UNKNOWN_RETURN_EXPRESSION + UNKNOWN_RETURN_EXPRESSION, + UnknownValue } from '../nodes/shared/Expression'; import type { ObjectPath, ObjectPathKey } from '../utils/PathTracker'; import { @@ -71,6 +74,22 @@ export default class ParameterVariable extends LocalVariable { } } + literalValue: LiteralValue | null = null; + setKnownLiteralValue(value: LiteralValue): void { + this.literalValue = value; + } + + getLiteralValueAtPath( + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + if (this.isReassigned) { + return UnknownValue; + } + return this.literalValue ?? UnknownValue; + } + deoptimizeArgumentsOnInteractionAtPath(interaction: NodeInteraction, path: ObjectPath): void { // For performance reasons, we fully deoptimize all deeper interactions if ( diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 521ff1f9b71..88633cec476 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -6,6 +6,7 @@ import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; import type Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; +import type { NodeBase } from '../nodes/shared/Node'; import type { VariableKind } from '../nodes/shared/VariableKinds'; import type { ObjectPath } from '../utils/PathTracker'; @@ -34,6 +35,12 @@ export default class Variable extends ExpressionEntity { */ addReference(_identifier: Identifier): void {} + AllUsedPlaces: NodeBase[] = []; + + addUsedPlace(identifier: NodeBase): void { + this.AllUsedPlaces.push(identifier); + } + /** * Prevent this variable from being renamed to this name to avoid name * collisions diff --git a/src/utils/functionParameterPass.ts b/src/utils/functionParameterPass.ts new file mode 100644 index 00000000000..0ec49b2ffb9 --- /dev/null +++ b/src/utils/functionParameterPass.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type Module from '../Module'; +import type CallExpression from '../ast/nodes/CallExpression'; +import type FunctionDeclaration from '../ast/nodes/FunctionDeclaration'; +import type { LiteralValue } from '../ast/nodes/Literal'; +import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../ast/utils/PathTracker'; +import LocalVariable from '../ast/variables/LocalVariable'; +import ParameterVariable from '../ast/variables/ParameterVariable'; + +function collectTopLevelFunctionCalls(modules: Module[]) { + const topLevelFunctions = new Map(); + modules = modules.filter(module => module.dynamicImporters.length === 0); + for (const module of modules) { + const scope = module.scope.variables; + for (const [_, v] of scope) { + if (!(v instanceof LocalVariable)) continue; + if (v.kind !== 'function') continue; + const allUses = v.AllUsedPlaces; + const containNonCallExpression = allUses.some(use => use.parent.type !== 'CallExpression'); + if (containNonCallExpression) continue; + const function_ = v.declarations[0].parent as FunctionDeclaration; + const allParameterIsIdentifier = function_.params.every( + parameter => parameter.type === 'Identifier' + ); + if (!allParameterIsIdentifier) continue; + topLevelFunctions.set( + function_, + allUses.map(use => use.parent as CallExpression) + ); + } + } + return topLevelFunctions; +} + +function setKnownLiteralValue(topLevelFunctions: Map) { + let changed = false; + const deleteFunctions: Set = new Set(); + + for (const [function_, calls] of topLevelFunctions) { + let parameterLength = function_.params.length; + for (const call of calls) { + parameterLength = Math.min(parameterLength, call.arguments.length); + } + for (let index = 0; index < parameterLength; index++) { + const parameter = function_.params[index]; + const literalValues: LiteralValue[] = []; + let allLiteral = true; + for (const call of calls) { + const literal = call.arguments[index].getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + call + ); + if (typeof literal === 'symbol') { + allLiteral = false; + break; + } + literalValues.push(literal); + } + + if (!allLiteral) continue; + + const allSame = literalValues.every(literal => literal === literalValues[0]); + if (!allSame) continue; + + if (parameter.variable instanceof ParameterVariable) { + changed = true; + deleteFunctions.add(function_); + parameter.variable.setKnownLiteralValue(literalValues[0]); + } + } + } + + for (const function_ of deleteFunctions) { + topLevelFunctions.delete(function_); + } + return changed; +} + +export function functionParameterPass(modules: Module[]) { + const topLevelFunctions = collectTopLevelFunctionCalls(modules); + let changed = true; + while (changed) { + changed = setKnownLiteralValue(topLevelFunctions); + } +} From 61bbfa025c9c76e6dd5a3272d4a8bb8ac4f7b3f9 Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 24 Mar 2024 00:51:48 +0800 Subject: [PATCH 02/49] fix: update old tests (for tree-shaking const param) --- .../_expected/amd/dep1.js | 5 +++-- .../_expected/cjs/dep1.js | 5 +++-- .../missing-export-reused-deconflicting/_expected/es/dep1.js | 5 +++-- .../_expected/system/dep1.js | 5 +++-- .../samples/missing-export-reused-deconflicting/dep1.js | 3 ++- .../_expected/amd/_virtual/_commonjsHelpers.js | 2 +- .../_expected/cjs/_virtual/_commonjsHelpers.js | 2 +- .../_expected/es/_virtual/_commonjsHelpers.js | 2 +- .../_expected/system/_virtual/_commonjsHelpers.js | 2 +- test/form/samples/deopt-string-concatenation/_expected.js | 1 + test/form/samples/deopt-string-concatenation/main.js | 1 + .../samples/side-effects-return-statements/_expected/amd.js | 1 + .../samples/side-effects-return-statements/_expected/cjs.js | 1 + .../samples/side-effects-return-statements/_expected/es.js | 1 + .../samples/side-effects-return-statements/_expected/iife.js | 1 + .../side-effects-return-statements/_expected/system.js | 1 + .../samples/side-effects-return-statements/_expected/umd.js | 1 + test/form/samples/side-effects-return-statements/main.js | 1 + 18 files changed, 27 insertions(+), 13 deletions(-) diff --git a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/amd/dep1.js b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/amd/dep1.js index 1ebb9552c37..c0bb33a6273 100644 --- a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/amd/dep1.js +++ b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/amd/dep1.js @@ -5,12 +5,13 @@ define(['exports'], (function (exports) { 'use strict'; console.log('This is the output when a missing export is used internally but not reexported'); function almostUseUnused(useIt) { - if (useIt) { + { + console.log(useIt); console.log(_missingExportShim); } } - almostUseUnused(false); + almostUseUnused(true); exports.missing1 = _missingExportShim; diff --git a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/cjs/dep1.js b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/cjs/dep1.js index a024bcd6f7f..31b08b56a1f 100644 --- a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/cjs/dep1.js +++ b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/cjs/dep1.js @@ -5,11 +5,12 @@ var _missingExportShim = void 0; console.log('This is the output when a missing export is used internally but not reexported'); function almostUseUnused(useIt) { - if (useIt) { + { + console.log(useIt); console.log(_missingExportShim); } } -almostUseUnused(false); +almostUseUnused(true); exports.missing1 = _missingExportShim; diff --git a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/es/dep1.js b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/es/dep1.js index 99f4196bf72..d9c8b65f05c 100644 --- a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/es/dep1.js +++ b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/es/dep1.js @@ -3,11 +3,12 @@ var _missingExportShim = void 0; console.log('This is the output when a missing export is used internally but not reexported'); function almostUseUnused(useIt) { - if (useIt) { + { + console.log(useIt); console.log(_missingExportShim); } } -almostUseUnused(false); +almostUseUnused(true); export { _missingExportShim as missing1 }; diff --git a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/system/dep1.js b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/system/dep1.js index fb77b0059b6..24aa7ab54ea 100644 --- a/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/system/dep1.js +++ b/test/chunking-form/samples/missing-export-reused-deconflicting/_expected/system/dep1.js @@ -8,12 +8,13 @@ System.register([], (function (exports) { console.log('This is the output when a missing export is used internally but not reexported'); function almostUseUnused(useIt) { - if (useIt) { + { + console.log(useIt); console.log(_missingExportShim); } } - almostUseUnused(false); + almostUseUnused(true); exports("missing1", _missingExportShim); diff --git a/test/chunking-form/samples/missing-export-reused-deconflicting/dep1.js b/test/chunking-form/samples/missing-export-reused-deconflicting/dep1.js index 84e22e0fe8f..f0751ef4cf6 100644 --- a/test/chunking-form/samples/missing-export-reused-deconflicting/dep1.js +++ b/test/chunking-form/samples/missing-export-reused-deconflicting/dep1.js @@ -2,8 +2,9 @@ console.log('This is the output when a missing export is used internally but not function almostUseUnused(useIt) { if (useIt) { + console.log(useIt); console.log(_missingExportShim); } } -almostUseUnused(false); +almostUseUnused(true); diff --git a/test/chunking-form/samples/preserve-modules-commonjs/_expected/amd/_virtual/_commonjsHelpers.js b/test/chunking-form/samples/preserve-modules-commonjs/_expected/amd/_virtual/_commonjsHelpers.js index f35ac4ef0d7..25ed6cf3721 100644 --- a/test/chunking-form/samples/preserve-modules-commonjs/_expected/amd/_virtual/_commonjsHelpers.js +++ b/test/chunking-form/samples/preserve-modules-commonjs/_expected/amd/_virtual/_commonjsHelpers.js @@ -1,7 +1,7 @@ define(['exports'], (function (exports) { 'use strict'; function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + return x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } exports.getDefaultExportFromCjs = getDefaultExportFromCjs; diff --git a/test/chunking-form/samples/preserve-modules-commonjs/_expected/cjs/_virtual/_commonjsHelpers.js b/test/chunking-form/samples/preserve-modules-commonjs/_expected/cjs/_virtual/_commonjsHelpers.js index ceeaa4d077f..fc314aed4db 100644 --- a/test/chunking-form/samples/preserve-modules-commonjs/_expected/cjs/_virtual/_commonjsHelpers.js +++ b/test/chunking-form/samples/preserve-modules-commonjs/_expected/cjs/_virtual/_commonjsHelpers.js @@ -1,7 +1,7 @@ 'use strict'; function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + return x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } exports.getDefaultExportFromCjs = getDefaultExportFromCjs; diff --git a/test/chunking-form/samples/preserve-modules-commonjs/_expected/es/_virtual/_commonjsHelpers.js b/test/chunking-form/samples/preserve-modules-commonjs/_expected/es/_virtual/_commonjsHelpers.js index 7b7c5f4f531..5fea8b90999 100644 --- a/test/chunking-form/samples/preserve-modules-commonjs/_expected/es/_virtual/_commonjsHelpers.js +++ b/test/chunking-form/samples/preserve-modules-commonjs/_expected/es/_virtual/_commonjsHelpers.js @@ -1,5 +1,5 @@ function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + return x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } export { getDefaultExportFromCjs }; diff --git a/test/chunking-form/samples/preserve-modules-commonjs/_expected/system/_virtual/_commonjsHelpers.js b/test/chunking-form/samples/preserve-modules-commonjs/_expected/system/_virtual/_commonjsHelpers.js index dd2c5886a6d..15f3676f92e 100644 --- a/test/chunking-form/samples/preserve-modules-commonjs/_expected/system/_virtual/_commonjsHelpers.js +++ b/test/chunking-form/samples/preserve-modules-commonjs/_expected/system/_virtual/_commonjsHelpers.js @@ -6,7 +6,7 @@ System.register([], (function (exports) { exports("getDefaultExportFromCjs", getDefaultExportFromCjs); function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + return x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } }) diff --git a/test/form/samples/deopt-string-concatenation/_expected.js b/test/form/samples/deopt-string-concatenation/_expected.js index 68a30dc4f66..42785de7340 100644 --- a/test/form/samples/deopt-string-concatenation/_expected.js +++ b/test/form/samples/deopt-string-concatenation/_expected.js @@ -9,3 +9,4 @@ function parseInt(str, radix) { } console.log(parseInt('1')); +console.log(parseInt(Symbol('1'))); diff --git a/test/form/samples/deopt-string-concatenation/main.js b/test/form/samples/deopt-string-concatenation/main.js index 68a30dc4f66..42785de7340 100644 --- a/test/form/samples/deopt-string-concatenation/main.js +++ b/test/form/samples/deopt-string-concatenation/main.js @@ -9,3 +9,4 @@ function parseInt(str, radix) { } console.log(parseInt('1')); +console.log(parseInt(Symbol('1'))); diff --git a/test/form/samples/side-effects-return-statements/_expected/amd.js b/test/form/samples/side-effects-return-statements/_expected/amd.js index dcdb995d37a..1633b0ab0e9 100644 --- a/test/form/samples/side-effects-return-statements/_expected/amd.js +++ b/test/form/samples/side-effects-return-statements/_expected/amd.js @@ -8,5 +8,6 @@ define((function () { 'use strict'; } assert.equal( isUsed( true ), 2 ); + assert.equal( isUsed( false ), 1 ); })); diff --git a/test/form/samples/side-effects-return-statements/_expected/cjs.js b/test/form/samples/side-effects-return-statements/_expected/cjs.js index 7dd6e813034..93d4ccf36c7 100644 --- a/test/form/samples/side-effects-return-statements/_expected/cjs.js +++ b/test/form/samples/side-effects-return-statements/_expected/cjs.js @@ -8,3 +8,4 @@ function isUsed ( x ) { } assert.equal( isUsed( true ), 2 ); +assert.equal( isUsed( false ), 1 ); diff --git a/test/form/samples/side-effects-return-statements/_expected/es.js b/test/form/samples/side-effects-return-statements/_expected/es.js index 38f357c97fc..9898b0d0f8b 100644 --- a/test/form/samples/side-effects-return-statements/_expected/es.js +++ b/test/form/samples/side-effects-return-statements/_expected/es.js @@ -6,3 +6,4 @@ function isUsed ( x ) { } assert.equal( isUsed( true ), 2 ); +assert.equal( isUsed( false ), 1 ); diff --git a/test/form/samples/side-effects-return-statements/_expected/iife.js b/test/form/samples/side-effects-return-statements/_expected/iife.js index c9da6fbbcd3..81b64ec8112 100644 --- a/test/form/samples/side-effects-return-statements/_expected/iife.js +++ b/test/form/samples/side-effects-return-statements/_expected/iife.js @@ -9,5 +9,6 @@ } assert.equal( isUsed( true ), 2 ); + assert.equal( isUsed( false ), 1 ); })(); diff --git a/test/form/samples/side-effects-return-statements/_expected/system.js b/test/form/samples/side-effects-return-statements/_expected/system.js index 968b341638b..096bcc73f08 100644 --- a/test/form/samples/side-effects-return-statements/_expected/system.js +++ b/test/form/samples/side-effects-return-statements/_expected/system.js @@ -11,6 +11,7 @@ System.register('myBundle', [], (function () { } assert.equal( isUsed( true ), 2 ); + assert.equal( isUsed( false ), 1 ); }) }; diff --git a/test/form/samples/side-effects-return-statements/_expected/umd.js b/test/form/samples/side-effects-return-statements/_expected/umd.js index 97a5be31699..fc3ff304af8 100644 --- a/test/form/samples/side-effects-return-statements/_expected/umd.js +++ b/test/form/samples/side-effects-return-statements/_expected/umd.js @@ -11,5 +11,6 @@ } assert.equal( isUsed( true ), 2 ); + assert.equal( isUsed( false ), 1 ); })); diff --git a/test/form/samples/side-effects-return-statements/main.js b/test/form/samples/side-effects-return-statements/main.js index 8e6b3b54f4f..a1e68e5d63f 100644 --- a/test/form/samples/side-effects-return-statements/main.js +++ b/test/form/samples/side-effects-return-statements/main.js @@ -15,3 +15,4 @@ function isUsed ( x ) { } assert.equal( isUsed( true ), 2 ); +assert.equal( isUsed( false ), 1 ); From 7050e85b72941e02ed0378f9d892b4814fa26571 Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 24 Mar 2024 01:56:46 +0800 Subject: [PATCH 03/49] test: add test for tree-shaking by propagate const parameter --- .../import-function/_config.js | 3 +++ .../import-function/_expected.js | 12 ++++++++++++ .../import-function/main.js | 16 ++++++++++++++++ .../indirect-import-function/_config.js | 3 +++ .../indirect-import-function/_expected.js | 7 +++++++ .../indirect-import-function/main.js | 3 +++ .../indirect-import-function/module1.js | 3 +++ .../indirect-import-function/module2.js | 5 +++++ .../recursion-literal/_config.js | 3 +++ .../recursion-literal/_expected.js | 13 +++++++++++++ .../recursion-literal/main.js | 13 +++++++++++++ .../reexport-function/_config.js | 3 +++ .../reexport-function/_expected.js | 7 +++++++ .../reexport-function/main.js | 3 +++ .../reexport-function/module1.js | 1 + .../reexport-function/module2.js | 5 +++++ 16 files changed, 100 insertions(+) create mode 100644 test/form/samples/tree-shake-literal-parameter/import-function/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/import-function/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/import-function/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/reexport-function/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/reexport-function/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/reexport-function/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/reexport-function/module1.js create mode 100644 test/form/samples/tree-shake-literal-parameter/reexport-function/module2.js diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_config.js b/test/form/samples/tree-shake-literal-parameter/import-function/_config.js new file mode 100644 index 00000000000..e4a54b5f90f --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'set parameters to literal if all calls are literal' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js new file mode 100644 index 00000000000..01151965941 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -0,0 +1,12 @@ +function add1(a, b, enable) { + { + return a + b; + } +} + +function add2(a, b, enable) { + return a - b; +} + +console.log(add1(1, 2)); +console.log(add2(1, 2)); \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js new file mode 100644 index 00000000000..ca43970bbce --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -0,0 +1,16 @@ +function add1(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} + +function add2(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} + +console.log(add1(1, 2, true)); +console.log(add2(1, 2, false)); diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_config.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_config.js new file mode 100644 index 00000000000..43454f74be2 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'parameters of indirect function import should also be tree-shaken' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js new file mode 100644 index 00000000000..d2cc14422b7 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js @@ -0,0 +1,7 @@ +function module2Func(enable) { + { + return 'module2Func'; + } +} + +console.log(module2Func()); diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js new file mode 100644 index 00000000000..2d4ea2e4b51 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js @@ -0,0 +1,3 @@ +import { module2Func } from './module1.js' + +console.log(module2Func(true)) diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js new file mode 100644 index 00000000000..a90138720ac --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js @@ -0,0 +1,3 @@ +import { module2Func } from './module2.js' + +export { module2Func } diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js new file mode 100644 index 00000000000..c66b12de017 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js @@ -0,0 +1,5 @@ +export function module2Func(enable) { + if (enable) { + return 'module2Func'; + } +} diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal/_config.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal/_config.js new file mode 100644 index 00000000000..251df7a6a12 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'function parameters tree-shaken should be recursive if literal' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal/_expected.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal/_expected.js new file mode 100644 index 00000000000..2fcab0594bc --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal/_expected.js @@ -0,0 +1,13 @@ +function fun1(enable) { + { + return 'fun1'; + } +} + +function fun2(enable) { + { + return fun1(); + } +} + +console.log(fun2()); \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal/main.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal/main.js new file mode 100644 index 00000000000..1187cc47fb3 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal/main.js @@ -0,0 +1,13 @@ +function fun1(enable) { + if (enable) { + return 'fun1'; + } +} + +function fun2(enable) { + if (enable) { + return fun1(enable); + } +} + +console.log(fun2(true)); \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/reexport-function/_config.js b/test/form/samples/tree-shake-literal-parameter/reexport-function/_config.js new file mode 100644 index 00000000000..74efd8ee3ad --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/reexport-function/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'function parameters tree-shaken should support reexport' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/reexport-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/reexport-function/_expected.js new file mode 100644 index 00000000000..fd105290ab6 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/reexport-function/_expected.js @@ -0,0 +1,7 @@ +function module2 (enable) { + { + return 'module2' + } +} + +console.log(module2()); \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/reexport-function/main.js b/test/form/samples/tree-shake-literal-parameter/reexport-function/main.js new file mode 100644 index 00000000000..c83b279bb08 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/reexport-function/main.js @@ -0,0 +1,3 @@ +import { module2 } from './module1.js' + +console.log(module2(true)) \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/reexport-function/module1.js b/test/form/samples/tree-shake-literal-parameter/reexport-function/module1.js new file mode 100644 index 00000000000..01e152738e8 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/reexport-function/module1.js @@ -0,0 +1 @@ +export * from './module2.js'; \ No newline at end of file diff --git a/test/form/samples/tree-shake-literal-parameter/reexport-function/module2.js b/test/form/samples/tree-shake-literal-parameter/reexport-function/module2.js new file mode 100644 index 00000000000..c4f4cacafd1 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/reexport-function/module2.js @@ -0,0 +1,5 @@ +export function module2 (enable) { + if (enable) { + return 'module2' + } +} \ No newline at end of file From fdb84cb37ea72d41d00d532434969dc1e279e1c5 Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 24 Mar 2024 07:17:43 +0800 Subject: [PATCH 04/49] feat&perf: support object param --- src/ast/variables/ParameterVariable.ts | 19 ++++++----- src/utils/functionParameterPass.ts | 32 ++++++++++++++++--- .../recursion-literal-object/_config.js | 3 ++ .../recursion-literal-object/_expected.js | 17 ++++++++++ .../recursion-literal-object/main.js | 21 ++++++++++++ 5 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/recursion-literal-object/main.js diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index ce056aa0abb..72079bd878d 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -5,7 +5,6 @@ import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_CALLED } from '../NodeInteractions'; import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; -import type { LiteralValue } from '../nodes/Literal'; import type { ExpressionEntity, LiteralValueOrUnknown } from '../nodes/shared/Expression'; import { deoptimizeInteraction, @@ -13,6 +12,7 @@ import { UNKNOWN_RETURN_EXPRESSION, UnknownValue } from '../nodes/shared/Expression'; +import type { ExpressionNode } from '../nodes/shared/Node'; import type { ObjectPath, ObjectPathKey } from '../utils/PathTracker'; import { PathTracker, @@ -74,20 +74,23 @@ export default class ParameterVariable extends LocalVariable { } } - literalValue: LiteralValue | null = null; - setKnownLiteralValue(value: LiteralValue): void { - this.literalValue = value; + knownValue: ExpressionNode | null = null; + setKnownValue(value: ExpressionNode): void { + this.knownValue = value; } getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity ): LiteralValueOrUnknown { if (this.isReassigned) { return UnknownValue; } - return this.literalValue ?? UnknownValue; + if (this.knownValue) { + return this.knownValue.getLiteralValueAtPath(path, recursionTracker, origin); + } + return UnknownValue; } deoptimizeArgumentsOnInteractionAtPath(interaction: NodeInteraction, path: ObjectPath): void { diff --git a/src/utils/functionParameterPass.ts b/src/utils/functionParameterPass.ts index 0ec49b2ffb9..602771bda09 100644 --- a/src/utils/functionParameterPass.ts +++ b/src/utils/functionParameterPass.ts @@ -3,6 +3,7 @@ import type Module from '../Module'; import type CallExpression from '../ast/nodes/CallExpression'; import type FunctionDeclaration from '../ast/nodes/FunctionDeclaration'; import type { LiteralValue } from '../ast/nodes/Literal'; +import type { ExpressionNode } from '../ast/nodes/shared/Node'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../ast/utils/PathTracker'; import LocalVariable from '../ast/variables/LocalVariable'; import ParameterVariable from '../ast/variables/ParameterVariable'; @@ -15,23 +16,44 @@ function collectTopLevelFunctionCalls(modules: Module[]) { for (const [_, v] of scope) { if (!(v instanceof LocalVariable)) continue; if (v.kind !== 'function') continue; + const allUses = v.AllUsedPlaces; + if (allUses.length === 0) continue; + const containNonCallExpression = allUses.some(use => use.parent.type !== 'CallExpression'); if (containNonCallExpression) continue; + const function_ = v.declarations[0].parent as FunctionDeclaration; + if (function_.params.length === 0) continue; + const allParameterIsIdentifier = function_.params.every( parameter => parameter.type === 'Identifier' ); if (!allParameterIsIdentifier) continue; - topLevelFunctions.set( - function_, - allUses.map(use => use.parent as CallExpression) - ); + + if (allUses.length === 1) { + forwardFunctionUsedOnce(function_, allUses[0].parent as CallExpression); + } else { + topLevelFunctions.set( + function_, + allUses.map(use => use.parent as CallExpression) + ); + } } } return topLevelFunctions; } +function forwardFunctionUsedOnce(function_: FunctionDeclaration, call: CallExpression) { + const maxLength = Math.min(function_.params.length, call.arguments.length); + for (let index = 0; index < maxLength; index++) { + const parameter = function_.params[index]; + if (parameter.variable instanceof ParameterVariable) { + parameter.variable.setKnownValue(call.arguments[index] as ExpressionNode); + } + } +} + function setKnownLiteralValue(topLevelFunctions: Map) { let changed = false; const deleteFunctions: Set = new Set(); @@ -66,7 +88,7 @@ function setKnownLiteralValue(topLevelFunctions: Map Date: Sun, 24 Mar 2024 19:48:24 +0800 Subject: [PATCH 05/49] style: update coverage --- src/utils/functionParameterPass.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/utils/functionParameterPass.ts b/src/utils/functionParameterPass.ts index 602771bda09..dd5deb7835e 100644 --- a/src/utils/functionParameterPass.ts +++ b/src/utils/functionParameterPass.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type Module from '../Module'; import type CallExpression from '../ast/nodes/CallExpression'; import type FunctionDeclaration from '../ast/nodes/FunctionDeclaration'; @@ -14,8 +13,7 @@ function collectTopLevelFunctionCalls(modules: Module[]) { for (const module of modules) { const scope = module.scope.variables; for (const [_, v] of scope) { - if (!(v instanceof LocalVariable)) continue; - if (v.kind !== 'function') continue; + if (!(v instanceof LocalVariable) || v.kind !== 'function') continue; const allUses = v.AllUsedPlaces; if (allUses.length === 0) continue; @@ -27,7 +25,8 @@ function collectTopLevelFunctionCalls(modules: Module[]) { if (function_.params.length === 0) continue; const allParameterIsIdentifier = function_.params.every( - parameter => parameter.type === 'Identifier' + parameter => + parameter.type === 'Identifier' && parameter.variable instanceof ParameterVariable ); if (!allParameterIsIdentifier) continue; @@ -47,10 +46,8 @@ function collectTopLevelFunctionCalls(modules: Module[]) { function forwardFunctionUsedOnce(function_: FunctionDeclaration, call: CallExpression) { const maxLength = Math.min(function_.params.length, call.arguments.length); for (let index = 0; index < maxLength; index++) { - const parameter = function_.params[index]; - if (parameter.variable instanceof ParameterVariable) { - parameter.variable.setKnownValue(call.arguments[index] as ExpressionNode); - } + const parameterVariable = function_.params[index].variable as ParameterVariable; + parameterVariable.setKnownValue(call.arguments[index] as ExpressionNode); } } @@ -85,11 +82,10 @@ function setKnownLiteralValue(topLevelFunctions: Map literal === literalValues[0]); if (!allSame) continue; - if (parameter.variable instanceof ParameterVariable) { - changed = true; - deleteFunctions.add(function_); - parameter.variable.setKnownValue(calls[0].arguments[index] as ExpressionNode); - } + changed = true; + deleteFunctions.add(function_); + const parameterVariable = parameter.variable as ParameterVariable; + parameterVariable.setKnownValue(calls[0].arguments[index] as ExpressionNode); } } From 7381847d03192bb66b6680a56341fa56cb66008a Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 24 Mar 2024 20:16:46 +0800 Subject: [PATCH 06/49] test: update tree-shake-literal-parameter --- .../recursion-literal-object/_expected.js | 9 ++++++++ .../recursion-literal-object/main.js | 23 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js index c94eaadae7f..7dd00524d0f 100644 --- a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js @@ -10,8 +10,17 @@ function fun2(options) { } } +function fun4(options) { + if (options.enable) ; else { + console.log('func4'); + } +} + console.log( fun2({ enable: true + }), + fun4({ + enable: false }) ); diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/main.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/main.js index edb7a180e12..4f845cc4e2c 100644 --- a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/main.js +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/main.js @@ -2,7 +2,7 @@ function fun1(options) { if (options.enable) { return 'fun1'; } else { - console.log(222); + console.log('func1'); } } @@ -10,12 +10,31 @@ function fun2(options) { if (options.enable) { return fun1(options); } else { - console.log(111); + console.log('func2'); + } +} + +function fun3(options) { + if (options.enable) { + return 'fun3'; + } else { + console.log('func3'); + } +} + +function fun4(options) { + if (options.enable) { + return fun3(options); + } else { + console.log('func4'); } } console.log( fun2({ enable: true + }), + fun4({ + enable: false }) ); From 87f82de221363788e9be22a3026d17f5f9dc4c9f Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 20:37:14 +0800 Subject: [PATCH 07/49] test: update tree-shake top export --- .../import-function/lib.js | 13 +++++++++++++ .../import-function/main.js | 14 +------------- .../top-level-export/_config.js | 3 +++ .../top-level-export/_expected.js | 9 +++++++++ .../top-level-export/main.js | 7 +++++++ 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/import-function/lib.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/main.js diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js new file mode 100644 index 00000000000..19a433356d0 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js @@ -0,0 +1,13 @@ +export function add1(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} + +export function add2(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index ca43970bbce..f6e332bd84d 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,16 +1,4 @@ -function add1(a, b, enable) { - if (enable) { - return a + b; - } - return a - b; -} - -function add2(a, b, enable) { - if (enable) { - return a + b; - } - return a - b; -} +import { add1, add2 } from './lib.js' console.log(add1(1, 2, true)); console.log(add2(1, 2, false)); diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js new file mode 100644 index 00000000000..ee38cd4752f --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'The exported function at top level should not be optimized' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js new file mode 100644 index 00000000000..1f7c7c05da2 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js @@ -0,0 +1,9 @@ +function foo(x) { + // The exported function might also be called with "false" + if (x) console.log('true'); + else console.log('false'); +} + +foo(true); + +export { foo }; diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js new file mode 100644 index 00000000000..1908f783d07 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js @@ -0,0 +1,7 @@ +export function foo(x) { + // The exported function might also be called with "false" + if (x) console.log('true'); + else console.log('false') +} + +foo(true); From cdcf53cc4e1f18b29d68557cf5c58dba2a2726cc Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 20:38:12 +0800 Subject: [PATCH 08/49] refactor: tree-shaking-literal --- src/Graph.ts | 2 - src/ast/nodes/IfStatement.ts | 16 ++-- src/ast/nodes/shared/FunctionBase.ts | 1 + src/ast/nodes/shared/FunctionNode.ts | 89 ++++++++++++++++++++- src/ast/variables/ParameterVariable.ts | 7 +- src/ast/variables/Variable.ts | 18 +++-- src/utils/functionParameterPass.ts | 104 ------------------------- 7 files changed, 115 insertions(+), 122 deletions(-) delete mode 100644 src/utils/functionParameterPass.ts diff --git a/src/Graph.ts b/src/Graph.ts index b0a66dd4e7f..fc47bfa3c54 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -18,7 +18,6 @@ import { PluginDriver } from './utils/PluginDriver'; import Queue from './utils/Queue'; import { BuildPhase } from './utils/buildPhase'; import { analyseModuleExecution } from './utils/executionOrder'; -import { functionParameterPass } from './utils/functionParameterPass'; import { LOGLEVEL_WARN } from './utils/logging'; import { error, @@ -161,7 +160,6 @@ export default class Graph { } private includeStatements(): void { - functionParameterPass(this.modules); const entryModules = [...this.entryModules, ...this.implicitEntryModules]; for (const module of entryModules) { markModuleAndImpureDependenciesAsExecuted(module); diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 07651843b59..7847209b143 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -123,14 +123,14 @@ export default class IfStatement extends StatementBase implements DeoptimizableE protected applyDeoptimizations() {} private getTestValue(): LiteralValueOrUnknown { - if (this.testValue === unset) { - return (this.testValue = this.test.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - this - )); - } - return this.testValue; + // if (this.testValue === unset) { + return (this.testValue = this.test.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + )); + // } + // return this.testValue; } private includeKnownTest(context: InclusionContext, testValue: LiteralValueOrUnknown) { diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 30c24011c91..90688dec9ce 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -102,6 +102,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.isReassigned = true; } } } diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 7b33996150a..574d705832c 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -3,17 +3,38 @@ import type { NodeInteraction } from '../../NodeInteractions'; import { INTERACTION_CALLED } from '../../NodeInteractions'; import type ChildScope from '../../scopes/ChildScope'; import FunctionScope from '../../scopes/FunctionScope'; -import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { + EMPTY_PATH, + type ObjectPath, + type PathTracker, + SHARED_RECURSION_TRACKER +} from '../../utils/PathTracker'; +import type ParameterVariable from '../../variables/ParameterVariable'; import type BlockStatement from '../BlockStatement'; +import type CallExpression from '../CallExpression'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; +import RestElement from '../RestElement'; +import type SpreadElement from '../SpreadElement'; import type { ExpressionEntity } from './Expression'; import { UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; -import { type IncludeChildren } from './Node'; +import type { ExpressionNode, IncludeChildren } from './Node'; import { ObjectEntity } from './ObjectEntity'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import type { PatternNode } from './Pattern'; +type FunctionParameterState = + | { + kind: 'TOP'; + } + | { + kind: 'MID'; + expression: ExpressionNode | SpreadElement; + } + | { + kind: 'BOTTOM'; + }; + export default class FunctionNode extends FunctionBase { declare body: BlockStatement; declare id: IdentifierWithVariable | null; @@ -31,6 +52,64 @@ export default class FunctionNode extends FunctionBase { this.scope.thisVariable.addEntityToBeDeoptimized(this.constructedEntity); } + private knownParameters: FunctionParameterState[] = []; + initKnownParameters() { + if (this.knownParameters.length === 0) { + this.knownParameters = Array.from({ length: this.params.length }).map(() => ({ + kind: 'TOP' + })); + } + } + /** + * updated knownParameters when a call is made to this function + * @param newArguments arguments of the call + */ + updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { + this.initKnownParameters(); + for (let position = 0; position < newArguments.length; position++) { + const argument = newArguments[position]; + const parameter = this.params[position]; + if (!parameter || parameter instanceof RestElement) { + break; + } + const knownParameter = this.knownParameters[position]; + if (knownParameter.kind === 'TOP') { + this.knownParameters[position] = { expression: argument, kind: 'MID' }; + } else if (knownParameter.kind === 'MID') { + const knownLiteral = knownParameter.expression.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + knownParameter.expression.parent as CallExpression + ); + const newLiteral = argument.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + argument.parent as CallExpression + ); + const bothLiteral = typeof knownLiteral !== 'symbol' && typeof newLiteral !== 'symbol'; + if (bothLiteral && knownLiteral !== newLiteral) { + this.knownParameters[position] = { kind: 'BOTTOM' }; + } else if (!bothLiteral) { + // if two call with same object: foo(bar);foo(bar); + this.knownParameters[position] = + knownParameter.expression === argument ? knownParameter : { kind: 'BOTTOM' }; + } // else both are the same literal, no need to update + } + } + } + + applyFunctionParameterOptimization() { + for (let position = 0; position < this.params.length; position++) { + const knownParameter = this.knownParameters[position]; + const parameter = this.params[position]; + if (knownParameter.kind === 'MID' && parameter instanceof Identifier) { + (parameter.variable as ParameterVariable).setKnownValue( + knownParameter.expression as ExpressionNode + ); + } + } + } + deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, path: ObjectPath, @@ -91,6 +170,12 @@ export default class FunctionNode extends FunctionBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + if (this.id?.variable.onlyFunctionCallUsed && this.id.variable.argumentsList.length > 0) { + for (let index = 0; index < this.id.variable.argumentsList.length; index++) { + this.updateKnownArguments(this.id.variable.argumentsList[index]); + } + this.applyFunctionParameterOptimization(); + } super.include(context, includeChildrenRecursively); this.id?.include(); const hasArguments = this.scope.argumentsVariable.included; diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 72079bd878d..405d69bd2ca 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -88,7 +88,12 @@ export default class ParameterVariable extends LocalVariable { return UnknownValue; } if (this.knownValue) { - return this.knownValue.getLiteralValueAtPath(path, recursionTracker, origin); + return recursionTracker.withTrackedEntityAtPath( + path, + this.knownValue, + () => this.knownValue!.getLiteralValueAtPath(path, recursionTracker, origin), + UnknownValue + ); } return UnknownValue; } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 88633cec476..b48eded2d3b 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -4,9 +4,11 @@ import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; +import type CallExpression from '../nodes/CallExpression'; import type Identifier from '../nodes/Identifier'; +import type SpreadElement from '../nodes/SpreadElement'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import type { NodeBase } from '../nodes/shared/Node'; +import type { ExpressionNode, NodeBase } from '../nodes/shared/Node'; import type { VariableKind } from '../nodes/shared/VariableKinds'; import type { ObjectPath } from '../utils/PathTracker'; @@ -35,10 +37,16 @@ export default class Variable extends ExpressionEntity { */ addReference(_identifier: Identifier): void {} - AllUsedPlaces: NodeBase[] = []; - - addUsedPlace(identifier: NodeBase): void { - this.AllUsedPlaces.push(identifier); + onlyFunctionCallUsed = true; + argumentsList: (ExpressionNode | SpreadElement)[][] = []; + addUsedPlace(usedPlace: NodeBase): void { + const isFunctionCall = usedPlace.parent.type === 'CallExpression'; + if (isFunctionCall) { + const callExpression = usedPlace.parent as CallExpression; + this.argumentsList.push(callExpression.arguments); + } else { + this.onlyFunctionCallUsed = false; + } } /** diff --git a/src/utils/functionParameterPass.ts b/src/utils/functionParameterPass.ts deleted file mode 100644 index dd5deb7835e..00000000000 --- a/src/utils/functionParameterPass.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type Module from '../Module'; -import type CallExpression from '../ast/nodes/CallExpression'; -import type FunctionDeclaration from '../ast/nodes/FunctionDeclaration'; -import type { LiteralValue } from '../ast/nodes/Literal'; -import type { ExpressionNode } from '../ast/nodes/shared/Node'; -import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../ast/utils/PathTracker'; -import LocalVariable from '../ast/variables/LocalVariable'; -import ParameterVariable from '../ast/variables/ParameterVariable'; - -function collectTopLevelFunctionCalls(modules: Module[]) { - const topLevelFunctions = new Map(); - modules = modules.filter(module => module.dynamicImporters.length === 0); - for (const module of modules) { - const scope = module.scope.variables; - for (const [_, v] of scope) { - if (!(v instanceof LocalVariable) || v.kind !== 'function') continue; - - const allUses = v.AllUsedPlaces; - if (allUses.length === 0) continue; - - const containNonCallExpression = allUses.some(use => use.parent.type !== 'CallExpression'); - if (containNonCallExpression) continue; - - const function_ = v.declarations[0].parent as FunctionDeclaration; - if (function_.params.length === 0) continue; - - const allParameterIsIdentifier = function_.params.every( - parameter => - parameter.type === 'Identifier' && parameter.variable instanceof ParameterVariable - ); - if (!allParameterIsIdentifier) continue; - - if (allUses.length === 1) { - forwardFunctionUsedOnce(function_, allUses[0].parent as CallExpression); - } else { - topLevelFunctions.set( - function_, - allUses.map(use => use.parent as CallExpression) - ); - } - } - } - return topLevelFunctions; -} - -function forwardFunctionUsedOnce(function_: FunctionDeclaration, call: CallExpression) { - const maxLength = Math.min(function_.params.length, call.arguments.length); - for (let index = 0; index < maxLength; index++) { - const parameterVariable = function_.params[index].variable as ParameterVariable; - parameterVariable.setKnownValue(call.arguments[index] as ExpressionNode); - } -} - -function setKnownLiteralValue(topLevelFunctions: Map) { - let changed = false; - const deleteFunctions: Set = new Set(); - - for (const [function_, calls] of topLevelFunctions) { - let parameterLength = function_.params.length; - for (const call of calls) { - parameterLength = Math.min(parameterLength, call.arguments.length); - } - for (let index = 0; index < parameterLength; index++) { - const parameter = function_.params[index]; - const literalValues: LiteralValue[] = []; - let allLiteral = true; - for (const call of calls) { - const literal = call.arguments[index].getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - call - ); - if (typeof literal === 'symbol') { - allLiteral = false; - break; - } - literalValues.push(literal); - } - - if (!allLiteral) continue; - - const allSame = literalValues.every(literal => literal === literalValues[0]); - if (!allSame) continue; - - changed = true; - deleteFunctions.add(function_); - const parameterVariable = parameter.variable as ParameterVariable; - parameterVariable.setKnownValue(calls[0].arguments[index] as ExpressionNode); - } - } - - for (const function_ of deleteFunctions) { - topLevelFunctions.delete(function_); - } - return changed; -} - -export function functionParameterPass(modules: Module[]) { - const topLevelFunctions = collectTopLevelFunctionCalls(modules); - let changed = true; - while (changed) { - changed = setKnownLiteralValue(topLevelFunctions); - } -} From 1c91f75083ae4f1cfe143c183059d39a5ed05bd8 Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 20:44:12 +0800 Subject: [PATCH 09/49] fix: test indent --- .../top-level-export/_expected.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js index 1f7c7c05da2..cda6394d3d3 100644 --- a/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js @@ -1,7 +1,7 @@ function foo(x) { - // The exported function might also be called with "false" - if (x) console.log('true'); - else console.log('false'); + // The exported function might also be called with "false" + if (x) console.log('true'); + else console.log('false'); } foo(true); From 3c9a810c53e4e6dbbfe2550496b769ad50bf24d8 Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 22:30:05 +0800 Subject: [PATCH 10/49] perf: remove same object SPJ getObjectEntity is private, so we can't judge if two object are the same --- src/ast/nodes/shared/FunctionNode.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 574d705832c..fdd1ad3ce08 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -87,12 +87,8 @@ export default class FunctionNode extends FunctionBase { argument.parent as CallExpression ); const bothLiteral = typeof knownLiteral !== 'symbol' && typeof newLiteral !== 'symbol'; - if (bothLiteral && knownLiteral !== newLiteral) { + if (!bothLiteral || knownLiteral !== newLiteral) { this.knownParameters[position] = { kind: 'BOTTOM' }; - } else if (!bothLiteral) { - // if two call with same object: foo(bar);foo(bar); - this.knownParameters[position] = - knownParameter.expression === argument ? knownParameter : { kind: 'BOTTOM' }; } // else both are the same literal, no need to update } } From 6e2e8662b29016b83903d2e324e6cca41eed3c1a Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 23:50:52 +0800 Subject: [PATCH 11/49] refactor: support iife --- src/ast/nodes/ArrowFunctionExpression.ts | 6 ++ src/ast/nodes/shared/FunctionBase.ts | 80 ++++++++++++++++++++- src/ast/nodes/shared/FunctionNode.ts | 88 +++--------------------- src/ast/variables/ParameterVariable.ts | 2 +- src/ast/variables/Variable.ts | 10 +-- 5 files changed, 96 insertions(+), 90 deletions(-) diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 8bf2df38572..aac9828bce8 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -68,6 +68,12 @@ export default class ArrowFunctionExpression extends FunctionBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + if (this.allArguments.length > 0) { + for (const argumentsList of this.allArguments) { + this.updateKnownArguments(argumentsList); + } + this.applyFunctionParameterOptimization(); + } super.include(context, includeChildrenRecursively); for (const parameter of this.params) { if (!(parameter instanceof Identifier)) { diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 90688dec9ce..536cca06301 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -9,9 +9,15 @@ 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 type ParameterVariable from '../../variables/ParameterVariable'; import BlockStatement from '../BlockStatement'; +import type CallExpression from '../CallExpression'; import Identifier from '../Identifier'; import RestElement from '../RestElement'; import type SpreadElement from '../SpreadElement'; @@ -27,6 +33,18 @@ import { import type { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; +type FunctionParameterState = + | { + kind: 'TOP'; + } + | { + kind: 'MID'; + expression: ExpressionNode | SpreadElement; + } + | { + kind: 'BOTTOM'; + }; + export default abstract class FunctionBase extends NodeBase { declare body: BlockStatement | ExpressionNode; declare params: PatternNode[]; @@ -57,6 +75,65 @@ export default abstract class FunctionBase extends NodeBase { this.flags = setFlag(this.flags, Flag.generator, value); } + private knownParameters: FunctionParameterState[] = []; + protected allArguments: (ExpressionNode | SpreadElement)[][] = []; + initKnownParameters() { + if (this.knownParameters.length === 0) { + this.knownParameters = Array.from({ length: this.params.length }).map(() => ({ + kind: 'TOP' + })); + } + } + /** + * updated knownParameters when a call is made to this function + * @param newArguments arguments of the call + */ + updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { + this.initKnownParameters(); + for (let position = 0; position < newArguments.length; position++) { + const argument = newArguments[position]; + const parameter = this.params[position]; + if (!parameter || parameter instanceof RestElement) { + break; + } + const knownParameter = this.knownParameters[position]; + if (knownParameter.kind === 'TOP') { + this.knownParameters[position] = { expression: argument, kind: 'MID' }; + } else if (knownParameter.kind === 'MID') { + if (knownParameter.expression === argument) { + continue; + } + const knownLiteral = knownParameter.expression.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + knownParameter.expression.parent as CallExpression + ); + const newLiteral = argument.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + argument.parent as CallExpression + ); + const bothLiteral = typeof knownLiteral !== 'symbol' && typeof newLiteral !== 'symbol'; + if (!bothLiteral || knownLiteral !== newLiteral) { + this.knownParameters[position] = { kind: 'BOTTOM' }; + } // else both are the same literal, no need to update + } + } + } + + applyFunctionParameterOptimization() { + for (let position = 0; position < this.params.length; position++) { + const knownParameter = this.knownParameters[position]; + const parameter = this.params[position]; + const ParameterVariable = parameter.variable as ParameterVariable | null; + if (knownParameter.kind === 'MID' && parameter instanceof Identifier) { + ParameterVariable?.setKnownValue(knownParameter.expression as ExpressionNode); + } else { + ParameterVariable?.setKnownValue(null); + } + } + } + protected objectEntity: ObjectEntity | null = null; deoptimizeArgumentsOnInteractionAtPath( @@ -84,6 +161,7 @@ export default abstract class FunctionBase extends NodeBase { this.addArgumentToBeDeoptimized(argument); } } + this.allArguments.push(args.slice(1) as (ExpressionNode | SpreadElement)[]); } else { this.getObjectEntity().deoptimizeArgumentsOnInteractionAtPath( interaction, diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index fdd1ad3ce08..9838a824f4e 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -3,38 +3,17 @@ import type { NodeInteraction } from '../../NodeInteractions'; import { INTERACTION_CALLED } from '../../NodeInteractions'; import type ChildScope from '../../scopes/ChildScope'; import FunctionScope from '../../scopes/FunctionScope'; -import { - EMPTY_PATH, - type ObjectPath, - type PathTracker, - SHARED_RECURSION_TRACKER -} from '../../utils/PathTracker'; -import type ParameterVariable from '../../variables/ParameterVariable'; +import { type ObjectPath, type PathTracker } from '../../utils/PathTracker'; import type BlockStatement from '../BlockStatement'; -import type CallExpression from '../CallExpression'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; -import RestElement from '../RestElement'; -import type SpreadElement from '../SpreadElement'; import type { ExpressionEntity } from './Expression'; import { UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; -import type { ExpressionNode, IncludeChildren } from './Node'; +import type { IncludeChildren } from './Node'; import { ObjectEntity } from './ObjectEntity'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import type { PatternNode } from './Pattern'; -type FunctionParameterState = - | { - kind: 'TOP'; - } - | { - kind: 'MID'; - expression: ExpressionNode | SpreadElement; - } - | { - kind: 'BOTTOM'; - }; - export default class FunctionNode extends FunctionBase { declare body: BlockStatement; declare id: IdentifierWithVariable | null; @@ -52,60 +31,6 @@ export default class FunctionNode extends FunctionBase { this.scope.thisVariable.addEntityToBeDeoptimized(this.constructedEntity); } - private knownParameters: FunctionParameterState[] = []; - initKnownParameters() { - if (this.knownParameters.length === 0) { - this.knownParameters = Array.from({ length: this.params.length }).map(() => ({ - kind: 'TOP' - })); - } - } - /** - * updated knownParameters when a call is made to this function - * @param newArguments arguments of the call - */ - updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { - this.initKnownParameters(); - for (let position = 0; position < newArguments.length; position++) { - const argument = newArguments[position]; - const parameter = this.params[position]; - if (!parameter || parameter instanceof RestElement) { - break; - } - const knownParameter = this.knownParameters[position]; - if (knownParameter.kind === 'TOP') { - this.knownParameters[position] = { expression: argument, kind: 'MID' }; - } else if (knownParameter.kind === 'MID') { - const knownLiteral = knownParameter.expression.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - knownParameter.expression.parent as CallExpression - ); - const newLiteral = argument.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - argument.parent as CallExpression - ); - const bothLiteral = typeof knownLiteral !== 'symbol' && typeof newLiteral !== 'symbol'; - if (!bothLiteral || knownLiteral !== newLiteral) { - this.knownParameters[position] = { kind: 'BOTTOM' }; - } // else both are the same literal, no need to update - } - } - } - - applyFunctionParameterOptimization() { - for (let position = 0; position < this.params.length; position++) { - const knownParameter = this.knownParameters[position]; - const parameter = this.params[position]; - if (knownParameter.kind === 'MID' && parameter instanceof Identifier) { - (parameter.variable as ParameterVariable).setKnownValue( - knownParameter.expression as ExpressionNode - ); - } - } - } - deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, path: ObjectPath, @@ -166,9 +91,12 @@ export default class FunctionNode extends FunctionBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (this.id?.variable.onlyFunctionCallUsed && this.id.variable.argumentsList.length > 0) { - for (let index = 0; index < this.id.variable.argumentsList.length; index++) { - this.updateKnownArguments(this.id.variable.argumentsList[index]); + if (this.id?.variable.onlyFunctionCallUsed && this.allArguments.length > 0) { + // each time tree-shake starts, we reoptimize the function parameters + // since it is a lattice analysis (the direction is one way, from TOP to BOTTOM) + // we are sure it will converge, and can use state from last iteration + for (const argumentsList of this.allArguments) { + this.updateKnownArguments(argumentsList); } this.applyFunctionParameterOptimization(); } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 405d69bd2ca..367500f704f 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -75,7 +75,7 @@ export default class ParameterVariable extends LocalVariable { } knownValue: ExpressionNode | null = null; - setKnownValue(value: ExpressionNode): void { + setKnownValue(value: ExpressionNode | null): void { this.knownValue = value; } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index b48eded2d3b..16a1ec5a779 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -4,11 +4,9 @@ import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; -import type CallExpression from '../nodes/CallExpression'; import type Identifier from '../nodes/Identifier'; -import type SpreadElement from '../nodes/SpreadElement'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import type { ExpressionNode, NodeBase } from '../nodes/shared/Node'; +import type { NodeBase } from '../nodes/shared/Node'; import type { VariableKind } from '../nodes/shared/VariableKinds'; import type { ObjectPath } from '../utils/PathTracker'; @@ -38,13 +36,9 @@ export default class Variable extends ExpressionEntity { addReference(_identifier: Identifier): void {} onlyFunctionCallUsed = true; - argumentsList: (ExpressionNode | SpreadElement)[][] = []; addUsedPlace(usedPlace: NodeBase): void { const isFunctionCall = usedPlace.parent.type === 'CallExpression'; - if (isFunctionCall) { - const callExpression = usedPlace.parent as CallExpression; - this.argumentsList.push(callExpression.arguments); - } else { + if (!isFunctionCall) { this.onlyFunctionCallUsed = false; } } From 8087111a7e29db03985563fdf9cf1fafcf98c5d2 Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 25 Mar 2024 23:51:12 +0800 Subject: [PATCH 12/49] test: tree-shake literal iife --- .../iife/_config.js | 3 +++ .../iife/_expected.js | 14 ++++++++++++++ .../tree-shake-literal-parameter/iife/main.js | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 test/form/samples/tree-shake-literal-parameter/iife/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/iife/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/iife/main.js diff --git a/test/form/samples/tree-shake-literal-parameter/iife/_config.js b/test/form/samples/tree-shake-literal-parameter/iife/_config.js new file mode 100644 index 00000000000..789458177ac --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/iife/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'tree-shake literal parameter for IIFE' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/iife/_expected.js b/test/form/samples/tree-shake-literal-parameter/iife/_expected.js new file mode 100644 index 00000000000..20e949b7bfc --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/iife/_expected.js @@ -0,0 +1,14 @@ +const result = ((enable) => { + { + return 'enabled'; + } +})(); + +const resultFunction = ((enable) => { + { + return 'enabled'; + } +}); + +console.log(result); +console.log(resultFunction()); diff --git a/test/form/samples/tree-shake-literal-parameter/iife/main.js b/test/form/samples/tree-shake-literal-parameter/iife/main.js new file mode 100644 index 00000000000..ae7100221ae --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/iife/main.js @@ -0,0 +1,18 @@ +const result = ((enable) => { + if (enable) { + return 'enabled'; + } else { + return 'disabled'; + } +})(true); + +const resultFunction = ((enable) => { + if (enable) { + return 'enabled'; + } else { + return 'disabled'; + } +}); + +console.log(result); +console.log(resultFunction(true)); \ No newline at end of file From b64f6491bf21421ed47c27939fefb48fcab415ab Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 06:58:37 +0800 Subject: [PATCH 13/49] fix: args but not callee should not be optimized --- src/ast/variables/Variable.ts | 5 ++++- .../import-function/_expected.js | 21 +++++++++++++++---- .../import-function/lib.js | 20 ++++++++++++------ .../import-function/main.js | 7 ++++++- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 16a1ec5a779..4c3b7ba8857 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -4,6 +4,7 @@ import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; +import type CallExpression from '../nodes/CallExpression'; import type Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; import type { NodeBase } from '../nodes/shared/Node'; @@ -37,7 +38,9 @@ export default class Variable extends ExpressionEntity { onlyFunctionCallUsed = true; addUsedPlace(usedPlace: NodeBase): void { - const isFunctionCall = usedPlace.parent.type === 'CallExpression'; + const isFunctionCall = + usedPlace.parent.type === 'CallExpression' && + (usedPlace.parent as CallExpression).callee === usedPlace; if (!isFunctionCall) { this.onlyFunctionCallUsed = false; } diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index 01151965941..c1fe79d59db 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -1,12 +1,25 @@ function add1(a, b, enable) { { - return a + b; - } + return a + b; + } } function add2(a, b, enable) { - return a - b; + return a - b; +} + +// keep it +function add3(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} + +function foo(bar) { + bar(); } console.log(add1(1, 2)); -console.log(add2(1, 2)); \ No newline at end of file +console.log(add2(1, 2)); +console.log(foo(add3)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js index 19a433356d0..963b3527d90 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js @@ -1,13 +1,21 @@ export function add1(a, b, enable) { if (enable) { - return a + b; - } - return a - b; + return a + b; + } + return a - b; } export function add2(a, b, enable) { if (enable) { - return a + b; - } - return a - b; + return a + b; + } + return a - b; +} + +// keep it +export function add3(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; } diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index f6e332bd84d..9894efcde82 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,4 +1,9 @@ -import { add1, add2 } from './lib.js' +import { add1, add2, add3 } from './lib.js' + +function foo(bar) { + bar(); +} console.log(add1(1, 2, true)); console.log(add2(1, 2, false)); +console.log(foo(add3)) From 5339c08838288fe0965ac3eea5f50fe55b4a94f4 Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 09:35:56 +0800 Subject: [PATCH 14/49] refactor: some logic to function base with comment --- src/ast/nodes/ArrowFunctionExpression.ts | 3 --- src/ast/nodes/shared/FunctionBase.ts | 9 +++++++++ src/ast/nodes/shared/FunctionNode.ts | 6 ------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index aac9828bce8..568d3623bcc 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -69,9 +69,6 @@ export default class ArrowFunctionExpression extends FunctionBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { if (this.allArguments.length > 0) { - for (const argumentsList of this.allArguments) { - this.updateKnownArguments(argumentsList); - } this.applyFunctionParameterOptimization(); } super.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 536cca06301..0ab475e6399 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -121,7 +121,16 @@ export default abstract class FunctionBase extends NodeBase { } } + /** + * each time tree-shake starts, this method should be called to reoptimize the parameters + * since it is a lattice analysis (the direction is one way, from TOP to BOTTOM) + * we are sure it will converge, and can use state from last iteration + */ applyFunctionParameterOptimization() { + // reoptimize all arguments, that's why we save them + for (const argumentsList of this.allArguments) { + this.updateKnownArguments(argumentsList); + } for (let position = 0; position < this.params.length; position++) { const knownParameter = this.knownParameters[position]; const parameter = this.params[position]; diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 9838a824f4e..ed446774dfd 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -92,12 +92,6 @@ export default class FunctionNode extends FunctionBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { if (this.id?.variable.onlyFunctionCallUsed && this.allArguments.length > 0) { - // each time tree-shake starts, we reoptimize the function parameters - // since it is a lattice analysis (the direction is one way, from TOP to BOTTOM) - // we are sure it will converge, and can use state from last iteration - for (const argumentsList of this.allArguments) { - this.updateKnownArguments(argumentsList); - } this.applyFunctionParameterOptimization(); } super.include(context, includeChildrenRecursively); From 35473881ecc86cda23ed8ebf6671d5788e87095c Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 14:09:36 +0800 Subject: [PATCH 15/49] feat&perf: support implicitly undefined --- src/ast/nodes/shared/FunctionBase.ts | 16 ++++++++++------ src/ast/nodes/shared/FunctionNode.ts | 4 ++-- src/ast/variables/ParameterVariable.ts | 5 ++--- test/form/samples/side-effect-h/_expected/amd.js | 1 + test/form/samples/side-effect-h/_expected/cjs.js | 1 + test/form/samples/side-effect-h/_expected/es.js | 1 + .../form/samples/side-effect-h/_expected/iife.js | 1 + .../samples/side-effect-h/_expected/system.js | 1 + test/form/samples/side-effect-h/_expected/umd.js | 1 + test/form/samples/side-effect-h/main.js | 1 + .../import-function/_expected.js | 2 +- .../import-function/main.js | 2 +- 12 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 0ab475e6399..bb805ba6404 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -15,6 +15,7 @@ import { UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker'; +import { UNDEFINED_EXPRESSION } from '../../values'; import type ParameterVariable from '../../variables/ParameterVariable'; import BlockStatement from '../BlockStatement'; import type CallExpression from '../CallExpression'; @@ -90,8 +91,8 @@ export default abstract class FunctionBase extends NodeBase { */ updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { this.initKnownParameters(); - for (let position = 0; position < newArguments.length; position++) { - const argument = newArguments[position]; + for (let position = 0; position < this.params.length; position++) { + const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; if (!parameter || parameter instanceof RestElement) { break; @@ -135,10 +136,13 @@ export default abstract class FunctionBase extends NodeBase { const knownParameter = this.knownParameters[position]; const parameter = this.params[position]; const ParameterVariable = parameter.variable as ParameterVariable | null; - if (knownParameter.kind === 'MID' && parameter instanceof Identifier) { - ParameterVariable?.setKnownValue(knownParameter.expression as ExpressionNode); - } else { - ParameterVariable?.setKnownValue(null); + // Parameters without default values + if (parameter instanceof Identifier) { + if (knownParameter.kind === 'MID') { + ParameterVariable?.setKnownValue(knownParameter.expression); + } else { + ParameterVariable?.setKnownValue(null); + } } } } diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index ed446774dfd..080a6c6664d 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -3,13 +3,13 @@ import type { NodeInteraction } from '../../NodeInteractions'; import { INTERACTION_CALLED } from '../../NodeInteractions'; import type ChildScope from '../../scopes/ChildScope'; import FunctionScope from '../../scopes/FunctionScope'; -import { type ObjectPath, type PathTracker } from '../../utils/PathTracker'; +import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; import type BlockStatement from '../BlockStatement'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; import type { ExpressionEntity } from './Expression'; import { UNKNOWN_EXPRESSION } from './Expression'; import FunctionBase from './FunctionBase'; -import type { IncludeChildren } from './Node'; +import { type IncludeChildren } from './Node'; import { ObjectEntity } from './ObjectEntity'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import type { PatternNode } from './Pattern'; diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 367500f704f..a4f57373e76 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -12,7 +12,6 @@ import { UNKNOWN_RETURN_EXPRESSION, UnknownValue } from '../nodes/shared/Expression'; -import type { ExpressionNode } from '../nodes/shared/Node'; import type { ObjectPath, ObjectPathKey } from '../utils/PathTracker'; import { PathTracker, @@ -74,8 +73,8 @@ export default class ParameterVariable extends LocalVariable { } } - knownValue: ExpressionNode | null = null; - setKnownValue(value: ExpressionNode | null): void { + knownValue: ExpressionEntity | null = null; + setKnownValue(value: ExpressionEntity | null): void { this.knownValue = value; } diff --git a/test/form/samples/side-effect-h/_expected/amd.js b/test/form/samples/side-effect-h/_expected/amd.js index d3a055b343a..e46b7cd7cb9 100644 --- a/test/form/samples/side-effect-h/_expected/amd.js +++ b/test/form/samples/side-effect-h/_expected/amd.js @@ -7,6 +7,7 @@ define((function () { 'use strict'; } foo(); + foo(true); var main = 42; diff --git a/test/form/samples/side-effect-h/_expected/cjs.js b/test/form/samples/side-effect-h/_expected/cjs.js index 1d437295c93..4862e7aa0e7 100644 --- a/test/form/samples/side-effect-h/_expected/cjs.js +++ b/test/form/samples/side-effect-h/_expected/cjs.js @@ -7,6 +7,7 @@ function foo ( ok ) { } foo(); +foo(true); var main = 42; diff --git a/test/form/samples/side-effect-h/_expected/es.js b/test/form/samples/side-effect-h/_expected/es.js index 1d5410d073d..39c215e330c 100644 --- a/test/form/samples/side-effect-h/_expected/es.js +++ b/test/form/samples/side-effect-h/_expected/es.js @@ -5,6 +5,7 @@ function foo ( ok ) { } foo(); +foo(true); var main = 42; diff --git a/test/form/samples/side-effect-h/_expected/iife.js b/test/form/samples/side-effect-h/_expected/iife.js index 083d37914dd..b6fc61fb49b 100644 --- a/test/form/samples/side-effect-h/_expected/iife.js +++ b/test/form/samples/side-effect-h/_expected/iife.js @@ -8,6 +8,7 @@ var myBundle = (function () { } foo(); + foo(true); var main = 42; diff --git a/test/form/samples/side-effect-h/_expected/system.js b/test/form/samples/side-effect-h/_expected/system.js index dcb761aa7ba..6ace3fb0f81 100644 --- a/test/form/samples/side-effect-h/_expected/system.js +++ b/test/form/samples/side-effect-h/_expected/system.js @@ -10,6 +10,7 @@ System.register('myBundle', [], (function (exports) { } foo(); + foo(true); var main = exports("default", 42); diff --git a/test/form/samples/side-effect-h/_expected/umd.js b/test/form/samples/side-effect-h/_expected/umd.js index 770859846a0..f72ccce31f6 100644 --- a/test/form/samples/side-effect-h/_expected/umd.js +++ b/test/form/samples/side-effect-h/_expected/umd.js @@ -11,6 +11,7 @@ } foo(); + foo(true); var main = 42; diff --git a/test/form/samples/side-effect-h/main.js b/test/form/samples/side-effect-h/main.js index 16d73469124..b736c13f944 100644 --- a/test/form/samples/side-effect-h/main.js +++ b/test/form/samples/side-effect-h/main.js @@ -5,5 +5,6 @@ function foo ( ok ) { } foo(); +foo(true); export default 42; diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index c1fe79d59db..0d869daac8a 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -21,5 +21,5 @@ function foo(bar) { } console.log(add1(1, 2)); -console.log(add2(1, 2)); +console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index 9894efcde82..2ebd9c0fa6f 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -5,5 +5,5 @@ function foo(bar) { } console.log(add1(1, 2, true)); -console.log(add2(1, 2, false)); +console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)) From 1d18ba1139508090bffe1789aa49e2246d933940 Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 16:14:30 +0800 Subject: [PATCH 16/49] test: tree-shake literal conditional --- .../import-function/_expected.js | 8 ++++++++ .../tree-shake-literal-parameter/import-function/lib.js | 8 ++++++++ .../tree-shake-literal-parameter/import-function/main.js | 3 ++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index 0d869daac8a..61a3273dd92 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -16,6 +16,13 @@ function add3(a, b, enable) { return a - b; } +// conditional expression +function add4(a, b, enable) { + { + return a + b; + } +} + function foo(bar) { bar(); } @@ -23,3 +30,4 @@ function foo(bar) { console.log(add1(1, 2)); console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)); +console.log(add4(1, 2)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js index 963b3527d90..d088f9cca86 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js @@ -19,3 +19,11 @@ export function add3(a, b, enable) { } return a - b; } + +// conditional expression +export function add4(a, b, enable) { + if (enable? true: false) { + return a + b; + } + return a - b; +} diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index 2ebd9c0fa6f..ec1ae7b8630 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,4 +1,4 @@ -import { add1, add2, add3 } from './lib.js' +import { add1, add2, add3, add4 } from './lib.js' function foo(bar) { bar(); @@ -7,3 +7,4 @@ function foo(bar) { console.log(add1(1, 2, true)); console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)) +console.log(add4(1, 2, true)); From ac4794dde37e6fdad4a4772757db9dd99ad291f9 Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 16:37:42 +0800 Subject: [PATCH 17/49] feat: integrate with optimizeCache --- src/ast/nodes/ConditionalExpression.ts | 15 ++++++----- src/ast/nodes/IfStatement.ts | 20 +++++++------- src/ast/nodes/LogicalExpression.ts | 27 ++++++++++--------- src/ast/nodes/MemberExpression.ts | 4 +-- src/ast/nodes/shared/CallExpressionBase.ts | 4 +-- src/ast/variables/LocalVariable.ts | 3 +-- src/ast/variables/ParameterVariable.ts | 8 ++++++ .../expression-cache/_config.js | 3 +++ .../expression-cache/_expected.js | 14 ++++++++++ .../expression-cache/main.js | 16 +++++++++++ 10 files changed, 78 insertions(+), 36 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/expression-cache/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/expression-cache/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/expression-cache/main.js diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 5645a23f910..88794a1ec09 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/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, @@ -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(); - } } } @@ -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); } diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 7847209b143..375c4eeaf04 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -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, @@ -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 { @@ -123,14 +123,14 @@ export default class IfStatement extends StatementBase implements DeoptimizableE protected applyDeoptimizations() {} private getTestValue(): LiteralValueOrUnknown { - // if (this.testValue === unset) { - return (this.testValue = this.test.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - this - )); - // } - // return this.testValue; + if (this.testValue === unset) { + return (this.testValue = this.test.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + )); + } + return this.testValue; } private includeKnownTest(context: InclusionContext, testValue: LiteralValueOrUnknown) { diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 55de1486181..34ce063059d 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/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, @@ -57,21 +57,22 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } deoptimizeCache(): void { + this.isBranchResolutionAnalysed = false; + const { + scope: { context }, + expressionsToBeDeoptimized + } = this; + 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); - const { - scope: { context }, - expressionsToBeDeoptimized - } = this; - this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - // Request another pass because we need to ensure "include" runs again if - // it is rendered - context.requestTreeshakingPass(); } } @@ -90,9 +91,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); } diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index f2ba221c147..ef395380c0c 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/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'; @@ -188,7 +188,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) { diff --git a/src/ast/nodes/shared/CallExpressionBase.ts b/src/ast/nodes/shared/CallExpressionBase.ts index 04c045b5d93..392424930f0 100644 --- a/src/ast/nodes/shared/CallExpressionBase.ts +++ b/src/ast/nodes/shared/CallExpressionBase.ts @@ -1,4 +1,4 @@ -import { EMPTY_ARRAY, EMPTY_SET } from '../../../utils/blank'; +import { EMPTY_SET } from '../../../utils/blank'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext } from '../../ExecutionContext'; import type { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions'; @@ -59,7 +59,7 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo this.returnExpression = UNKNOWN_RETURN_EXPRESSION; const { deoptimizableDependentExpressions, expressionsToBeDeoptimized } = this; this.expressionsToBeDeoptimized = EMPTY_SET; - this.deoptimizableDependentExpressions = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; + this.deoptimizableDependentExpressions = []; for (const expression of deoptimizableDependentExpressions) { expression.deoptimizeCache(); } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index fe5bd488492..8daff1a34c8 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -1,5 +1,4 @@ import type { AstContext, default as Module } from '../../Module'; -import { EMPTY_ARRAY } from '../../utils/blank'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { createInclusionContext } from '../ExecutionContext'; @@ -93,7 +92,7 @@ export default class LocalVariable extends Variable { if (path.length === 0) { this.isReassigned = true; const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; + this.expressionsToBeDeoptimized = []; for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index a4f57373e76..b66816a34f2 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -37,6 +37,7 @@ export default class ParameterVariable extends LocalVariable { private deoptimizations = new PathTracker(); private deoptimizedFields = new Set(); private entitiesToBeDeoptimized = new Set(); + private knownExpressionsToBeDeoptimized: DeoptimizableEntity[] = []; constructor( name: string, @@ -75,6 +76,12 @@ export default class ParameterVariable extends LocalVariable { knownValue: ExpressionEntity | null = null; setKnownValue(value: ExpressionEntity | null): void { + if (this.knownValue !== value) { + for (const expression of this.knownExpressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + this.knownExpressionsToBeDeoptimized = []; + } this.knownValue = value; } @@ -83,6 +90,7 @@ export default class ParameterVariable extends LocalVariable { recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { + this.knownExpressionsToBeDeoptimized.push(origin); if (this.isReassigned) { return UnknownValue; } diff --git a/test/form/samples/tree-shake-literal-parameter/expression-cache/_config.js b/test/form/samples/tree-shake-literal-parameter/expression-cache/_config.js new file mode 100644 index 00000000000..697aea54b46 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/expression-cache/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'should update cache of logical and conditional expressions' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/expression-cache/_expected.js b/test/form/samples/tree-shake-literal-parameter/expression-cache/_expected.js new file mode 100644 index 00000000000..8cee561823a --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/expression-cache/_expected.js @@ -0,0 +1,14 @@ +function add1(a, b, enable) { + { + return a + b; + } +} + +function add2(a, b, enable) { + { + return a + b; + } +} + +console.log(add1(1, 2)); +console.log(add2(1, 2)); diff --git a/test/form/samples/tree-shake-literal-parameter/expression-cache/main.js b/test/form/samples/tree-shake-literal-parameter/expression-cache/main.js new file mode 100644 index 00000000000..4c1f739a1d5 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/expression-cache/main.js @@ -0,0 +1,16 @@ +function add1(a, b, enable) { + if (enable? true: false) { + return a + b; + } + return a - b; +} + +function add2(a, b, enable) { + if (enable && 1) { + return a + b; + } + return a - b; +} + +console.log(add1(1, 2, true)); +console.log(add2(1, 2, true)); \ No newline at end of file From 9aa6a70e31c44c37d02a20b68302d0f4994c7132 Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 16:38:05 +0800 Subject: [PATCH 18/49] test: fix --- test/form/samples/supports-es6-shim/_expected.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/form/samples/supports-es6-shim/_expected.js b/test/form/samples/supports-es6-shim/_expected.js index ae2951820eb..84fb3b7f09d 100644 --- a/test/form/samples/supports-es6-shim/_expected.js +++ b/test/form/samples/supports-es6-shim/_expected.js @@ -2057,7 +2057,7 @@ var es6Shim = {exports: {}}; }; var withinULPDistance = function withinULPDistance(result, expected, distance) { - return _abs(1 - (result / expected)) / Number.EPSILON < (distance || 8); + return _abs(1 - (result / expected)) / Number.EPSILON < (8); }; defineProperties(Math, MathShims); From 9f536bf3f43306b314a36fc7487f8527a68d18d6 Mon Sep 17 00:00:00 2001 From: liuly Date: Tue, 26 Mar 2024 18:24:16 +0800 Subject: [PATCH 19/49] feat: function argument side effect --- src/ast/nodes/shared/FunctionBase.ts | 20 +++++++++++++ src/ast/variables/ParameterVariable.ts | 18 ++++++++++-- .../argument-assignment/_config.js | 3 ++ .../argument-assignment/_expected.js | 15 ++++++++++ .../argument-assignment/main.js | 15 ++++++++++ .../import-function/_expected.js | 2 +- .../import-function/main.js | 2 +- .../recursion-literal-object/_expected.js | 16 ++++------- .../side-effect/_config.js | 3 ++ .../side-effect/_expected.js | 22 +++++++++++++++ .../side-effect/main.js | 28 +++++++++++++++++++ .../deactivate-via-option/_expected.js | 4 +-- 12 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/argument-assignment/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/argument-assignment/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/argument-assignment/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/side-effect/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/side-effect/main.js diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index bb805ba6404..2b1151c17be 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -122,12 +122,32 @@ export default abstract class FunctionBase extends NodeBase { } } + forwardArgumentsForFunctionCalledOnce(newArguments: (SpreadElement | ExpressionNode)[]): void { + for (let position = 0; position < this.params.length; position++) { + const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; + const parameter = this.params[position]; + if (!parameter || parameter instanceof RestElement) { + break; + } + if (parameter instanceof Identifier) { + const ParameterVariable = parameter.variable as ParameterVariable | null; + ParameterVariable?.setKnownValue(argument, true); + } + } + } + /** * each time tree-shake starts, this method should be called to reoptimize the parameters * since it is a lattice analysis (the direction is one way, from TOP to BOTTOM) * we are sure it will converge, and can use state from last iteration */ applyFunctionParameterOptimization() { + if (this.allArguments.length === 1) { + // we are sure what knownParameters 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.updateKnownArguments(argumentsList); diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index b66816a34f2..679f210327f 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -1,8 +1,9 @@ import type { AstContext } from '../../Module'; import { EMPTY_ARRAY } from '../../utils/blank'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; +import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; -import { INTERACTION_CALLED } from '../NodeInteractions'; +import { INTERACTION_ASSIGNED, INTERACTION_CALLED } from '../NodeInteractions'; import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; import type { ExpressionEntity, LiteralValueOrUnknown } from '../nodes/shared/Expression'; @@ -75,7 +76,9 @@ export default class ParameterVariable extends LocalVariable { } knownValue: ExpressionEntity | null = null; - setKnownValue(value: ExpressionEntity | null): void { + argument: ExpressionEntity | null = null; + setKnownValue(value: ExpressionEntity | null, isOnlyArgument = false): void { + this.argument = isOnlyArgument ? value : null; if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -105,6 +108,17 @@ export default class ParameterVariable extends LocalVariable { return UnknownValue; } + hasEffectsOnInteractionAtPath( + path: ObjectPath, + interaction: NodeInteraction, + context: HasEffectsContext + ): boolean { + // assigned is a bit different, because we are in a new scope + return this.argument && interaction.type !== INTERACTION_ASSIGNED + ? this.argument.hasEffectsOnInteractionAtPath(path, interaction, context) + : super.hasEffectsOnInteractionAtPath(path, interaction, context); + } + deoptimizeArgumentsOnInteractionAtPath(interaction: NodeInteraction, path: ObjectPath): void { // For performance reasons, we fully deoptimize all deeper interactions if ( diff --git a/test/form/samples/tree-shake-literal-parameter/argument-assignment/_config.js b/test/form/samples/tree-shake-literal-parameter/argument-assignment/_config.js new file mode 100644 index 00000000000..1a210baea05 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/argument-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'argument assignment side-effect' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/argument-assignment/_expected.js b/test/form/samples/tree-shake-literal-parameter/argument-assignment/_expected.js new file mode 100644 index 00000000000..f7cb2c16d5a --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/argument-assignment/_expected.js @@ -0,0 +1,15 @@ +function add(a, b) { + let sum = 0; + for (let i = a; i < b; i++) { + sum += i; + } + return sum; +} + +function foo(a) { + // don't optimize to '0;' + a = 0; + console.log(a); +} + +console.log(foo(add(0, 100))); diff --git a/test/form/samples/tree-shake-literal-parameter/argument-assignment/main.js b/test/form/samples/tree-shake-literal-parameter/argument-assignment/main.js new file mode 100644 index 00000000000..d9b3646538f --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/argument-assignment/main.js @@ -0,0 +1,15 @@ +function add(a, b) { + let sum = 0; + for (let i = a; i < b; i++) { + sum += i; + } + return sum; +} + +function foo(a) { + // don't optimize to '0;' + a = 0; + console.log(a) +} + +console.log(foo(add(0, 100))) diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index 61a3273dd92..8721b960cd7 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -24,7 +24,7 @@ function add4(a, b, enable) { } function foo(bar) { - bar(); + console.log(bar()); } console.log(add1(1, 2)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index ec1ae7b8630..440ec102700 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,7 +1,7 @@ import { add1, add2, add3, add4 } from './lib.js' function foo(bar) { - bar(); + console.log(bar()); } console.log(add1(1, 2, true)); diff --git a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js index 7dd00524d0f..f909ce360ec 100644 --- a/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/recursion-literal-object/_expected.js @@ -1,26 +1,22 @@ function fun1(options) { - if (options.enable) { + { return 'fun1'; } } function fun2(options) { - if (options.enable) { - return fun1(options); + { + return fun1(); } } function fun4(options) { - if (options.enable) ; else { + { console.log('func4'); } } console.log( - fun2({ - enable: true - }), - fun4({ - enable: false - }) + fun2(), + fun4() ); diff --git a/test/form/samples/tree-shake-literal-parameter/side-effect/_config.js b/test/form/samples/tree-shake-literal-parameter/side-effect/_config.js new file mode 100644 index 00000000000..e06ff5cdc74 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/side-effect/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'test tree-shake-literal-parameter with side-effect' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js b/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js new file mode 100644 index 00000000000..1d1c50069a3 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js @@ -0,0 +1,22 @@ +function foo1() { + return 1; +} + +function bar1(foo) { + console.log(foo()); +} + +function bar2(foo) { +} + +// not pure +function foo3() { + console.log(1); +} + +function bar3(foo) { + foo(); +} + + +console.log(bar1(foo1), bar2(), bar3(foo3)); diff --git a/test/form/samples/tree-shake-literal-parameter/side-effect/main.js b/test/form/samples/tree-shake-literal-parameter/side-effect/main.js new file mode 100644 index 00000000000..8227a82f77a --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/side-effect/main.js @@ -0,0 +1,28 @@ +function foo1() { + return 1; +} + +function bar1(foo) { + console.log(foo()) +} + +// pure +function foo2() { + return 1; +} + +function bar2(foo) { + foo() +} + +// not pure +function foo3() { + console.log(1); +} + +function bar3(foo) { + foo() +} + + +console.log(bar1(foo1), bar2(foo2), bar3(foo3)) diff --git a/test/form/samples/try-statement-deoptimization/deactivate-via-option/_expected.js b/test/form/samples/try-statement-deoptimization/deactivate-via-option/_expected.js index a8d430c8fbb..7a1891aaca5 100644 --- a/test/form/samples/try-statement-deoptimization/deactivate-via-option/_expected.js +++ b/test/form/samples/try-statement-deoptimization/deactivate-via-option/_expected.js @@ -6,13 +6,11 @@ const mutated = {}; function test(callback) { try { - callback(); mutate(mutated); } catch {} } -test(() => { -}); +test(); try {} finally { console.log('retained'); From fd90a7be6eb3c971a946e99acbc9f7ee09a3002d Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 28 Mar 2024 14:19:15 +0800 Subject: [PATCH 20/49] style: revert export default change since deoptimizePath will detect --- src/ast/variables/ExportDefaultVariable.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/ast/variables/ExportDefaultVariable.ts b/src/ast/variables/ExportDefaultVariable.ts index 31d1b93d5a2..979405b5784 100644 --- a/src/ast/variables/ExportDefaultVariable.ts +++ b/src/ast/variables/ExportDefaultVariable.ts @@ -3,7 +3,6 @@ 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'; @@ -38,16 +37,6 @@ export default class ExportDefaultVariable extends LocalVariable { } } - addUsedPlace(identifier: NodeBase): void { - const original = this.getOriginalVariable(); - this.originalVariable = null; - if (original === this) { - super.addUsedPlace(identifier); - } else { - original.addUsedPlace(identifier); - } - } - forbidName(name: string) { const original = this.getOriginalVariable(); if (original === this) { @@ -68,7 +57,6 @@ export default class ExportDefaultVariable extends LocalVariable { getDirectOriginalVariable(): Variable | null { return this.originalId && - this.originalId.variable && (this.hasId || !( this.originalId.isPossibleTDZ() || From 4e3aee0f9d502e726021b438c79eaf235fbe76b1 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 28 Mar 2024 15:02:02 +0800 Subject: [PATCH 21/49] feat: support foo(bar);foo(bar); --- src/ast/nodes/shared/FunctionBase.ts | 9 ++++++++- src/ast/variables/ParameterVariable.ts | 15 +++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 2b1151c17be..1cbd5ba066e 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -104,6 +104,13 @@ export default abstract class FunctionBase extends NodeBase { if (knownParameter.expression === argument) { continue; } + if ( + knownParameter.expression instanceof Identifier && + argument instanceof Identifier && + knownParameter.expression.variable === argument.variable + ) { + continue; + } const knownLiteral = knownParameter.expression.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, @@ -131,7 +138,7 @@ export default abstract class FunctionBase extends NodeBase { } if (parameter instanceof Identifier) { const ParameterVariable = parameter.variable as ParameterVariable | null; - ParameterVariable?.setKnownValue(argument, true); + ParameterVariable?.setKnownValue(argument); } } } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 679f210327f..57aa4797f6a 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -76,9 +76,12 @@ export default class ParameterVariable extends LocalVariable { } knownValue: ExpressionEntity | null = null; - argument: ExpressionEntity | null = null; - setKnownValue(value: ExpressionEntity | null, isOnlyArgument = false): void { - this.argument = isOnlyArgument ? value : null; + /** + * If we are sure about the value of this parameter, we can set it here. + * It can be a literal or the only possible value of the parameter. + * @param value The known value of the parameter to be set. + */ + setKnownValue(value: ExpressionEntity | null): void { if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -113,9 +116,9 @@ export default class ParameterVariable extends LocalVariable { interaction: NodeInteraction, context: HasEffectsContext ): boolean { - // assigned is a bit different, because we are in a new scope - return this.argument && interaction.type !== INTERACTION_ASSIGNED - ? this.argument.hasEffectsOnInteractionAtPath(path, interaction, context) + // assigned is a bit different, since the value has a new name (the parameter) + return this.knownValue && interaction.type !== INTERACTION_ASSIGNED + ? this.knownValue.hasEffectsOnInteractionAtPath(path, interaction, context) : super.hasEffectsOnInteractionAtPath(path, interaction, context); } From 074e7e7e8e3b597e0d57338eec08216610069761 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 28 Mar 2024 15:02:30 +0800 Subject: [PATCH 22/49] test: add more side-effect and top-level test --- .../side-effect/_expected.js | 45 ++++++++++++++++- .../side-effect/main.js | 49 +++++++++++++++++-- .../top-level-export/_config.js | 3 -- .../top-level-export/_expected.js | 9 ---- .../top-level-export/main.js | 7 --- .../top-level/_config.js | 3 ++ .../top-level/_expected.js | 23 +++++++++ .../top-level/main.js | 23 +++++++++ 8 files changed, 138 insertions(+), 24 deletions(-) delete mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js delete mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js delete mode 100644 test/form/samples/tree-shake-literal-parameter/top-level-export/main.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/top-level/main.js diff --git a/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js b/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js index 1d1c50069a3..77275663462 100644 --- a/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/side-effect/_expected.js @@ -9,7 +9,7 @@ function bar1(foo) { function bar2(foo) { } -// not pure +// not pure, preserve function foo3() { console.log(1); } @@ -18,5 +18,46 @@ function bar3(foo) { foo(); } - console.log(bar1(foo1), bar2(), bar3(foo3)); + +const options = { + enable: 1 +}; + +const options2 = { + enable: 1 +}; + +function calledWithSameVariable(options) { + { + return 'enabled'; + } +} + +function calledWithDifferentVariable(options) { + if (options.enable) { + return 'enabled'; + } else { + return 'disabled'; + } +} + +// forward hasEffects to `options` +console.log(calledWithSameVariable(), calledWithSameVariable()); +// no optimization +console.log(calledWithDifferentVariable(options), calledWithDifferentVariable(options2)); + +const optionsBeModified = { + enable: 1 +}; + +function calledWithModifiedVariable(options) { + if (options.enable) { + return 'enabled'; + } else { + return 'disabled'; + } +} + +console.log(calledWithModifiedVariable(optionsBeModified)); +optionsBeModified.enable = 0; diff --git a/test/form/samples/tree-shake-literal-parameter/side-effect/main.js b/test/form/samples/tree-shake-literal-parameter/side-effect/main.js index 8227a82f77a..d7f3ba00148 100644 --- a/test/form/samples/tree-shake-literal-parameter/side-effect/main.js +++ b/test/form/samples/tree-shake-literal-parameter/side-effect/main.js @@ -6,7 +6,7 @@ function bar1(foo) { console.log(foo()) } -// pure +// pure, can be tree-shaken function foo2() { return 1; } @@ -15,7 +15,7 @@ function bar2(foo) { foo() } -// not pure +// not pure, preserve function foo3() { console.log(1); } @@ -24,5 +24,48 @@ function bar3(foo) { foo() } - console.log(bar1(foo1), bar2(foo2), bar3(foo3)) + +const options = { + enable: 1 +} + +const options2 = { + enable: 1 +} + +function calledWithSameVariable(options) { + if (options.enable) { + return 'enabled'; + } else { + return 'disabled'; + } +} + +function calledWithDifferentVariable(options) { + if (options.enable) { + return 'enabled'; + } else { + return 'disabled'; + } +} + +// forward hasEffects to `options` +console.log(calledWithSameVariable(options), calledWithSameVariable(options)) +// no optimization +console.log(calledWithDifferentVariable(options), calledWithDifferentVariable(options2)) + +const optionsBeModified = { + enable: 1 +} + +function calledWithModifiedVariable(options) { + if (options.enable) { + return 'enabled'; + } else { + return 'disabled'; + } +} + +console.log(calledWithModifiedVariable(optionsBeModified)) +optionsBeModified.enable = 0; diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js deleted file mode 100644 index ee38cd4752f..00000000000 --- a/test/form/samples/tree-shake-literal-parameter/top-level-export/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = defineTest({ - description: 'The exported function at top level should not be optimized' -}); diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js deleted file mode 100644 index cda6394d3d3..00000000000 --- a/test/form/samples/tree-shake-literal-parameter/top-level-export/_expected.js +++ /dev/null @@ -1,9 +0,0 @@ -function foo(x) { - // The exported function might also be called with "false" - if (x) console.log('true'); - else console.log('false'); -} - -foo(true); - -export { foo }; diff --git a/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js b/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js deleted file mode 100644 index 1908f783d07..00000000000 --- a/test/form/samples/tree-shake-literal-parameter/top-level-export/main.js +++ /dev/null @@ -1,7 +0,0 @@ -export function foo(x) { - // The exported function might also be called with "false" - if (x) console.log('true'); - else console.log('false') -} - -foo(true); diff --git a/test/form/samples/tree-shake-literal-parameter/top-level/_config.js b/test/form/samples/tree-shake-literal-parameter/top-level/_config.js new file mode 100644 index 00000000000..027f90cf36d --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'Top level exports and global variables should not be optimized' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/top-level/_expected.js b/test/form/samples/tree-shake-literal-parameter/top-level/_expected.js new file mode 100644 index 00000000000..70d050ab798 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level/_expected.js @@ -0,0 +1,23 @@ +function foo(x) { + // The exported function might also be called with "false" + if (x) console.log('true'); + else console.log('false'); +} + +// global variable should not be optimized +foo2 = (x) => { + if (x) console.log('true'); + else console.log('false'); +}; + +// export default should not be optimized +var main = foo3 = (x) => { + if (x) console.log('true'); + else console.log('false'); +}; + +foo(true); +foo2(true); +foo3(true); + +export { main as default, foo }; diff --git a/test/form/samples/tree-shake-literal-parameter/top-level/main.js b/test/form/samples/tree-shake-literal-parameter/top-level/main.js new file mode 100644 index 00000000000..9f706606d6f --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/top-level/main.js @@ -0,0 +1,23 @@ +function foo(x) { + // The exported function might also be called with "false" + if (x) console.log('true'); + else console.log('false'); +} + +// global variable should not be optimized +foo2 = (x) => { + if (x) console.log('true'); + else console.log('false'); +} + +// export default should not be optimized +export default foo3 = (x) => { + if (x) console.log('true'); + else console.log('false'); +} + +foo(true); +foo2(true); +foo3(true); + +export { foo }; From aabd6f0ed5fc25723d07d89506b33fdb8fa38902 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 28 Mar 2024 12:17:00 +0100 Subject: [PATCH 23/49] 4.13.2 --- CHANGELOG.md | 14 ++++++++++++++ browser/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28038571dc8..1ff18857977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # rollup changelog +## 4.13.2 + +_2024-03-28_ + +### Bug Fixes + +- Support now ppc64le architecture (#5350) +- Ensure accessing module info is cached after the build phase for improved performance (#5438) + +### Pull Requests + +- [#5350](https://github.com/rollup/rollup/pull/5350): Add support for ppc64le (@pavolloffay, @lukastaegert) +- [#5438](https://github.com/rollup/rollup/pull/5438): Cache module info getters before output generation (@bluwy, @lukastaegert) + ## 4.13.1 _2024-03-27_ diff --git a/browser/package.json b/browser/package.json index 731480ceb80..326289b2e15 100644 --- a/browser/package.json +++ b/browser/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/browser", - "version": "4.13.1", + "version": "4.13.2", "description": "Next-generation ES module bundler browser build", "main": "dist/rollup.browser.js", "module": "dist/es/rollup.browser.js", diff --git a/package-lock.json b/package-lock.json index f9b743b6e25..855d3e8de08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rollup", - "version": "4.13.1", + "version": "4.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rollup", - "version": "4.13.1", + "version": "4.13.2", "license": "MIT", "dependencies": { "@types/estree": "1.0.5" diff --git a/package.json b/package.json index e763f72c3d7..3c4c1693c0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "4.13.1", + "version": "4.13.2", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", From 912bacd888c18d4d9dcf6827ba3bbee1313640d8 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 28 Mar 2024 21:56:46 +0800 Subject: [PATCH 24/49] test: add export default test --- .../import-function/_expected.js | 8 ++++++++ .../tree-shake-literal-parameter/import-function/lib.js | 8 ++++++++ .../tree-shake-literal-parameter/import-function/main.js | 3 ++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index 8721b960cd7..5e068ced76d 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -1,3 +1,10 @@ +// export default +function add(a, b, enable) { + { + return a + b; + } +} + function add1(a, b, enable) { { return a + b; @@ -27,6 +34,7 @@ function foo(bar) { console.log(bar()); } +console.log(add(1, 2)); console.log(add1(1, 2)); console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js index d088f9cca86..c56fd400379 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js @@ -1,3 +1,11 @@ +// export default +export default function add(a, b, enable) { + if (enable) { + return a + b; + } + return a - b; +} + export function add1(a, b, enable) { if (enable) { return a + b; diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index 440ec102700..d675cd02c62 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,9 +1,10 @@ -import { add1, add2, add3, add4 } from './lib.js' +import add, { add1, add2, add3, add4 } from './lib.js' function foo(bar) { console.log(bar()); } +console.log(add(1, 2, true)); console.log(add1(1, 2, true)); console.log(add2(1, 2)); // unused should be treated as undefined console.log(foo(add3)) From e1143bc2decbe57af03760af44181724bfebf897 Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 29 Mar 2024 15:07:08 +0800 Subject: [PATCH 25/49] refactor FunctionParameterState and remove initalization --- src/ast/nodes/shared/FunctionBase.ts | 52 +++++++++----------------- src/ast/variables/ParameterVariable.ts | 5 ++- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 1cbd5ba066e..2a153c48c40 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -34,17 +34,8 @@ import { import type { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; -type FunctionParameterState = - | { - kind: 'TOP'; - } - | { - kind: 'MID'; - expression: ExpressionNode | SpreadElement; - } - | { - kind: 'BOTTOM'; - }; +const UnknownArgument = Symbol('Unknown Argument'); +type FunctionParameterState = ExpressionNode | SpreadElement | typeof UnknownArgument; export default abstract class FunctionBase extends NodeBase { declare body: BlockStatement | ExpressionNode; @@ -78,19 +69,11 @@ export default abstract class FunctionBase extends NodeBase { private knownParameters: FunctionParameterState[] = []; protected allArguments: (ExpressionNode | SpreadElement)[][] = []; - initKnownParameters() { - if (this.knownParameters.length === 0) { - this.knownParameters = Array.from({ length: this.params.length }).map(() => ({ - kind: 'TOP' - })); - } - } /** * updated knownParameters when a call is made to this function * @param newArguments arguments of the call */ updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { - this.initKnownParameters(); for (let position = 0; position < this.params.length; position++) { const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; @@ -98,32 +81,32 @@ export default abstract class FunctionBase extends NodeBase { break; } const knownParameter = this.knownParameters[position]; - if (knownParameter.kind === 'TOP') { - this.knownParameters[position] = { expression: argument, kind: 'MID' }; - } else if (knownParameter.kind === 'MID') { - if (knownParameter.expression === argument) { + if (knownParameter === undefined) { + this.knownParameters[position] = argument; + } else if (knownParameter !== UnknownArgument) { + // update knownParameter with argument + if (knownParameter === argument) { continue; } if ( - knownParameter.expression instanceof Identifier && + knownParameter instanceof Identifier && argument instanceof Identifier && - knownParameter.expression.variable === argument.variable + knownParameter.variable === argument.variable ) { continue; } - const knownLiteral = knownParameter.expression.getLiteralValueAtPath( + const knownLiteral = knownParameter.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, - knownParameter.expression.parent as CallExpression + knownParameter.parent as CallExpression ); const newLiteral = argument.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, argument.parent as CallExpression ); - const bothLiteral = typeof knownLiteral !== 'symbol' && typeof newLiteral !== 'symbol'; - if (!bothLiteral || knownLiteral !== newLiteral) { - this.knownParameters[position] = { kind: 'BOTTOM' }; + if (knownLiteral !== newLiteral || typeof knownLiteral === 'symbol') { + this.knownParameters[position] = UnknownArgument; } // else both are the same literal, no need to update } } @@ -145,7 +128,8 @@ export default abstract class FunctionBase extends NodeBase { /** * each time tree-shake starts, this method should be called to reoptimize the parameters - * since it is a lattice analysis (the direction is one way, from TOP to BOTTOM) + * 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 */ applyFunctionParameterOptimization() { @@ -165,10 +149,10 @@ export default abstract class FunctionBase extends NodeBase { const ParameterVariable = parameter.variable as ParameterVariable | null; // Parameters without default values if (parameter instanceof Identifier) { - if (knownParameter.kind === 'MID') { - ParameterVariable?.setKnownValue(knownParameter.expression); + if (knownParameter === UnknownArgument) { + ParameterVariable?.setKnownValue(undefined); } else { - ParameterVariable?.setKnownValue(null); + ParameterVariable?.setKnownValue(knownParameter); } } } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 57aa4797f6a..ef2c4cb4414 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -75,13 +75,14 @@ export default class ParameterVariable extends LocalVariable { } } - knownValue: ExpressionEntity | null = null; + knownValue: ExpressionEntity | undefined = undefined; /** * If we are sure about the value of this parameter, we can set it here. * It can be a literal or the only possible value of the parameter. + * an undefined value means that the parameter is not known. * @param value The known value of the parameter to be set. */ - setKnownValue(value: ExpressionEntity | null): void { + setKnownValue(value: ExpressionEntity | undefined): void { if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); From bfe6a6c4c55d8ce592eca42ccba0a9a0b920fb5a Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 09:07:53 +0800 Subject: [PATCH 26/49] refactor IIFE --- src/ast/nodes/ArrowFunctionExpression.ts | 12 +++++++++--- src/ast/nodes/shared/FunctionBase.ts | 15 ++++++++++++++- src/ast/nodes/shared/FunctionNode.ts | 8 +++++--- test/form/samples/supports-es5-shim/_expected.js | 2 -- .../iife/_expected.js | 10 +++++----- .../tree-shake-literal-parameter/iife/main.js | 10 +++++----- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 568d3623bcc..edde6f909f5 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -4,9 +4,11 @@ import { INTERACTION_CALLED } from '../NodeInteractions'; import type ChildScope from '../scopes/ChildScope'; import ReturnValueScope from '../scopes/ReturnValueScope'; import { type ObjectPath } from '../utils/PathTracker'; +import type Variable from '../variables/Variable'; import type BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import type * as NodeType from './NodeType'; +import VariableDeclarator from './VariableDeclarator'; import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import FunctionBase from './shared/FunctionBase'; import type { ExpressionNode, IncludeChildren } from './shared/Node'; @@ -67,10 +69,14 @@ export default class ArrowFunctionExpression extends FunctionBase { return false; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (this.allArguments.length > 0) { - this.applyFunctionParameterOptimization(); + getIdentifierVariable(): Variable | null { + if (this.parent instanceof VariableDeclarator) { + return this.parent.id.variable ?? null; } + return null; + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { super.include(context, includeChildrenRecursively); for (const parameter of this.params) { if (!(parameter instanceof Identifier)) { diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 2a153c48c40..2e22db87bb4 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -17,6 +17,7 @@ import { } 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 Identifier from '../Identifier'; @@ -68,7 +69,7 @@ export default abstract class FunctionBase extends NodeBase { } private knownParameters: FunctionParameterState[] = []; - protected allArguments: (ExpressionNode | SpreadElement)[][] = []; + private allArguments: (ExpressionNode | SpreadElement)[][] = []; /** * updated knownParameters when a call is made to this function * @param newArguments arguments of the call @@ -283,7 +284,19 @@ export default abstract class FunctionBase extends NodeBase { return false; } + getIdentifierVariable(): Variable | null { + return null; + } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + const isIIFE = + this.parent.type === 'CallExpression' && (this.parent as CallExpression).callee === this; + if ( + (isIIFE || this.getIdentifierVariable()?.onlyFunctionCallUsed) && + this.allArguments.length > 0 + ) { + this.applyFunctionParameterOptimization(); + } if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; const { brokenFlow } = context; diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 080a6c6664d..f2a1e756291 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -4,6 +4,7 @@ import { INTERACTION_CALLED } from '../../NodeInteractions'; import type ChildScope from '../../scopes/ChildScope'; import FunctionScope from '../../scopes/FunctionScope'; import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import type Variable from '../../variables/Variable'; import type BlockStatement from '../BlockStatement'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; import type { ExpressionEntity } from './Expression'; @@ -90,10 +91,11 @@ export default class FunctionNode extends FunctionBase { return false; } + getIdentifierVariable(): Variable | null { + return this.id?.variable ?? null; + } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - if (this.id?.variable.onlyFunctionCallUsed && this.allArguments.length > 0) { - this.applyFunctionParameterOptimization(); - } super.include(context, includeChildrenRecursively); this.id?.include(); const hasArguments = this.scope.argumentsVariable.included; diff --git a/test/form/samples/supports-es5-shim/_expected.js b/test/form/samples/supports-es5-shim/_expected.js index bc68debf94b..645169277b6 100644 --- a/test/form/samples/supports-es5-shim/_expected.js +++ b/test/form/samples/supports-es5-shim/_expected.js @@ -2202,7 +2202,6 @@ var es5Shim = {exports: {}}; // eslint-disable-next-line no-global-assign, no-implicit-globals parseInt = (function (origParseInt) { return function parseInt(str, radix) { - if (this instanceof parseInt) { new origParseInt(); } // eslint-disable-line new-cap, no-new, max-statements-per-line var string = trim(String(str)); var defaultedRadix = $Number(radix) || (hexRegex.test(string) ? 16 : 10); return origParseInt(string, defaultedRadix); @@ -2233,7 +2232,6 @@ var es5Shim = {exports: {}}; // eslint-disable-next-line no-global-assign, no-implicit-globals parseInt = (function (origParseInt) { return function parseInt(str, radix) { - if (this instanceof parseInt) { new origParseInt(); } // eslint-disable-line new-cap, no-new, max-statements-per-line var isSym = typeof str === 'symbol'; if (!isSym && str && typeof str === 'object') { try { diff --git a/test/form/samples/tree-shake-literal-parameter/iife/_expected.js b/test/form/samples/tree-shake-literal-parameter/iife/_expected.js index 20e949b7bfc..49705fc5935 100644 --- a/test/form/samples/tree-shake-literal-parameter/iife/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/iife/_expected.js @@ -1,14 +1,14 @@ -const result = ((enable) => { +const result1 = ((enable) => { { return 'enabled'; } })(); -const resultFunction = ((enable) => { +const result2 = (function (enable) { { return 'enabled'; } -}); +})(); -console.log(result); -console.log(resultFunction()); +console.log(result1); +console.log(result2); diff --git a/test/form/samples/tree-shake-literal-parameter/iife/main.js b/test/form/samples/tree-shake-literal-parameter/iife/main.js index ae7100221ae..4684874022c 100644 --- a/test/form/samples/tree-shake-literal-parameter/iife/main.js +++ b/test/form/samples/tree-shake-literal-parameter/iife/main.js @@ -1,4 +1,4 @@ -const result = ((enable) => { +const result1 = ((enable) => { if (enable) { return 'enabled'; } else { @@ -6,13 +6,13 @@ const result = ((enable) => { } })(true); -const resultFunction = ((enable) => { +const result2 = (function (enable) { if (enable) { return 'enabled'; } else { return 'disabled'; } -}); +})(true); -console.log(result); -console.log(resultFunction(true)); \ No newline at end of file +console.log(result1); +console.log(result2); \ No newline at end of file From c63e7450c7dbdae47ee00cbe6e43c155e0da4671 Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 11:15:07 +0800 Subject: [PATCH 27/49] feat: support export default anonymous --- src/ast/nodes/ArrowFunctionExpression.ts | 12 ++++-- src/ast/nodes/shared/FunctionBase.ts | 4 +- src/ast/nodes/shared/FunctionNode.ts | 10 ++++- .../import-function/_expected.js | 39 ++++++++++++++++--- .../import-function/arrow_lib.js | 30 ++++++++++++++ .../import-function/lib.js | 2 +- .../import-function/main.js | 10 ++++- 7 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/import-function/arrow_lib.js diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index edde6f909f5..d545e66e8b4 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -6,9 +6,10 @@ import ReturnValueScope from '../scopes/ReturnValueScope'; import { type ObjectPath } from '../utils/PathTracker'; import type Variable from '../variables/Variable'; import type BlockStatement from './BlockStatement'; +import type ExportDefaultDeclaration from './ExportDefaultDeclaration'; import Identifier from './Identifier'; import type * as NodeType from './NodeType'; -import VariableDeclarator from './VariableDeclarator'; +import type VariableDeclarator from './VariableDeclarator'; import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import FunctionBase from './shared/FunctionBase'; import type { ExpressionNode, IncludeChildren } from './shared/Node'; @@ -69,9 +70,12 @@ export default class ArrowFunctionExpression extends FunctionBase { return false; } - getIdentifierVariable(): Variable | null { - if (this.parent instanceof VariableDeclarator) { - return this.parent.id.variable ?? null; + getDeclarationVariable(): Variable | null { + if (this.parent.type === 'VariableDeclarator') { + return (this.parent as VariableDeclarator).id.variable ?? null; + } + if (this.parent.type === 'ExportDefaultDeclaration') { + return (this.parent as ExportDefaultDeclaration).variable; } return null; } diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 2e22db87bb4..fae6660c3e8 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -284,7 +284,7 @@ export default abstract class FunctionBase extends NodeBase { return false; } - getIdentifierVariable(): Variable | null { + getDeclarationVariable(): Variable | null { return null; } @@ -292,7 +292,7 @@ export default abstract class FunctionBase extends NodeBase { const isIIFE = this.parent.type === 'CallExpression' && (this.parent as CallExpression).callee === this; if ( - (isIIFE || this.getIdentifierVariable()?.onlyFunctionCallUsed) && + (isIIFE || this.getDeclarationVariable()?.onlyFunctionCallUsed) && this.allArguments.length > 0 ) { this.applyFunctionParameterOptimization(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index f2a1e756291..bb9a8a52168 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -6,6 +6,7 @@ import FunctionScope from '../../scopes/FunctionScope'; import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; import type Variable from '../../variables/Variable'; import type BlockStatement from '../BlockStatement'; +import type ExportDefaultDeclaration from '../ExportDefaultDeclaration'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; import type { ExpressionEntity } from './Expression'; import { UNKNOWN_EXPRESSION } from './Expression'; @@ -91,8 +92,13 @@ export default class FunctionNode extends FunctionBase { return false; } - getIdentifierVariable(): Variable | null { - return this.id?.variable ?? null; + getDeclarationVariable(): Variable | null { + return ( + this.id?.variable ?? + (this.parent.type === 'ExportDefaultDeclaration' + ? (this.parent as ExportDefaultDeclaration).variable + : null) + ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js index 5e068ced76d..f9d8a0fd937 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/_expected.js @@ -1,14 +1,12 @@ // export default -function add(a, b, enable) { +function add (a, b, enable) { { return a + b; } } function add1(a, b, enable) { - { - return a + b; - } + return a - b; } function add2(a, b, enable) { @@ -30,12 +28,43 @@ function add4(a, b, enable) { } } +// export default +var arrowAdd = (a, b, enable) => { + { + return a + b; + } +}; + +const arrowAdd1 = (a, b, enable) => { + return a - b; +}; + +// keep it +const arrowAdd2 = (a, b, enable) => { + if (enable) { + return a + b; + } + return a - b; +}; + +// conditional expression +const arrowAdd3 = (a, b, enable) => { + { + return a + b; + } +}; + function foo(bar) { console.log(bar()); } console.log(add(1, 2)); console.log(add1(1, 2)); -console.log(add2(1, 2)); // unused should be treated as undefined +console.log(add2(1, 2)); // unused argument should be treated as undefined console.log(foo(add3)); console.log(add4(1, 2)); + +console.log(arrowAdd(1, 2)); +console.log(arrowAdd1(1, 2)); +console.log(foo(arrowAdd2)); +console.log(arrowAdd3(1, 2)); diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/arrow_lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/arrow_lib.js new file mode 100644 index 00000000000..8b85efc44a7 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/import-function/arrow_lib.js @@ -0,0 +1,30 @@ +// export default +export default (a, b, enable) => { + if (enable) { + return a + b; + } + return a - b; +} + +export const arrowAdd1 = (a, b, enable) => { + if (enable) { + return a + b; + } + return a - b; +} + +// keep it +export const arrowAdd2 = (a, b, enable) => { + if (enable) { + return a + b; + } + return a - b; +} + +// conditional expression +export const arrowAdd3 = (a, b, enable) => { + if (enable? true: false) { + return a + b; + } + return a - b; +} diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js index c56fd400379..be4b8a63c76 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/lib.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/lib.js @@ -1,5 +1,5 @@ // export default -export default function add(a, b, enable) { +export default function (a, b, enable) { if (enable) { return a + b; } diff --git a/test/form/samples/tree-shake-literal-parameter/import-function/main.js b/test/form/samples/tree-shake-literal-parameter/import-function/main.js index d675cd02c62..fcd0b9dcbe1 100644 --- a/test/form/samples/tree-shake-literal-parameter/import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/import-function/main.js @@ -1,11 +1,17 @@ import add, { add1, add2, add3, add4 } from './lib.js' +import arrowAdd, { arrowAdd1, arrowAdd2, arrowAdd3 } from './arrow_lib.js' function foo(bar) { console.log(bar()); } console.log(add(1, 2, true)); -console.log(add1(1, 2, true)); -console.log(add2(1, 2)); // unused should be treated as undefined +console.log(add1(1, 2, false)); +console.log(add2(1, 2)); // unused argument should be treated as undefined console.log(foo(add3)) console.log(add4(1, 2, true)); + +console.log(arrowAdd(1, 2, true)); +console.log(arrowAdd1(1, 2, false)); +console.log(foo(arrowAdd2)); +console.log(arrowAdd3(1, 2, true)); \ No newline at end of file From 6be48de012b94b18504c4830be93d8cc51b47dda Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 12:09:47 +0800 Subject: [PATCH 28/49] fix: nested namespace tracking --- src/ast/nodes/MemberExpression.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index ef395380c0c..bc8453578de 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -156,9 +156,7 @@ export default class MemberExpression this.isUndefined = true; } else { this.variable = resolvedVariable; - if (this.object instanceof Identifier) { - this.variable.addUsedPlace(this); - } + this.variable.addUsedPlace(this); this.scope.addNamespaceMemberAccess(getStringFromPath(path!), resolvedVariable); } } else { From 9060ae8c5cadfacd29f903fc127ed45df2af6be5 Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 17:46:27 +0800 Subject: [PATCH 29/49] feat: support define then export default --- src/ast/nodes/ArrowFunctionExpression.ts | 13 -------- src/ast/nodes/FunctionDeclaration.ts | 5 ++++ src/ast/nodes/shared/FunctionBase.ts | 30 ++++++++++++++----- src/ast/nodes/shared/FunctionNode.ts | 11 ------- src/ast/variables/Variable.ts | 27 ++++++++++++++++- .../indirect-import-function/_expected.js | 13 ++++++-- .../define_and_export_default.js | 7 +++++ .../import_then_export.js | 3 ++ .../import_then_export_sub.js | 5 ++++ .../indirect-import-function/main.js | 6 ++-- .../indirect-import-function/module1.js | 3 -- .../indirect-import-function/module2.js | 5 ---- 12 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/define_and_export_default.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export.js create mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export_sub.js delete mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js delete mode 100644 test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index d545e66e8b4..8bf2df38572 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -4,12 +4,9 @@ import { INTERACTION_CALLED } from '../NodeInteractions'; import type ChildScope from '../scopes/ChildScope'; import ReturnValueScope from '../scopes/ReturnValueScope'; import { type ObjectPath } from '../utils/PathTracker'; -import type Variable from '../variables/Variable'; import type BlockStatement from './BlockStatement'; -import type ExportDefaultDeclaration from './ExportDefaultDeclaration'; import Identifier from './Identifier'; import type * as NodeType from './NodeType'; -import type VariableDeclarator from './VariableDeclarator'; import { Flag, isFlagSet, setFlag } from './shared/BitFlags'; import FunctionBase from './shared/FunctionBase'; import type { ExpressionNode, IncludeChildren } from './shared/Node'; @@ -70,16 +67,6 @@ export default class ArrowFunctionExpression extends FunctionBase { return false; } - getDeclarationVariable(): Variable | null { - if (this.parent.type === 'VariableDeclarator') { - return (this.parent as VariableDeclarator).id.variable ?? null; - } - if (this.parent.type === 'ExportDefaultDeclaration') { - return (this.parent as ExportDefaultDeclaration).variable; - } - return null; - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { super.include(context, includeChildrenRecursively); for (const parameter of this.params) { diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index 4f75fce15f4..3a84691a86f 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -1,4 +1,5 @@ import type ChildScope from '../scopes/ChildScope'; +import type Variable from '../variables/Variable'; import Identifier, { type IdentifierWithVariable } from './Identifier'; import type * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; @@ -14,6 +15,10 @@ export default class FunctionDeclaration extends FunctionNode { } } + getDeclarationVariable(): Variable | null { + return super.getDeclarationVariable() ?? this.id?.variable ?? null; + } + parseNode(esTreeNode: GenericEsTreeNode): this { if (esTreeNode.id !== null) { this.id = new Identifier(this, this.scope.parent as ChildScope).parseNode( diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index fae6660c3e8..a3276f1ecf8 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -20,9 +20,11 @@ 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 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'; @@ -99,13 +101,13 @@ export default abstract class FunctionBase extends NodeBase { const knownLiteral = knownParameter.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, - knownParameter.parent as CallExpression - ); - const newLiteral = argument.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - argument.parent as CallExpression + { + deoptimizeCache() {} + } ); + const newLiteral = argument.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { + deoptimizeCache() {} + }); if (knownLiteral !== newLiteral || typeof knownLiteral === 'symbol') { this.knownParameters[position] = UnknownArgument; } // else both are the same literal, no need to update @@ -284,7 +286,21 @@ export default abstract class FunctionBase extends NodeBase { return false; } + /** + * If the function is assigned/bound/... to some identifier declaration, + * getDeclarationVariable will return the Variable entity of that declaration. + * It can be used to track if all usages of this function are only function calls. + * While there are methods like deoptimizePath, + * this one can make sure 100% of usages are function calls. + * @returns the Variable entity of the declaration + */ getDeclarationVariable(): Variable | null { + if (this.parent.type === 'VariableDeclarator') { + return (this.parent as VariableDeclarator).id.variable ?? null; + } + if (this.parent.type === 'ExportDefaultDeclaration') { + return (this.parent as ExportDefaultDeclaration).variable; + } return null; } @@ -292,7 +308,7 @@ export default abstract class FunctionBase extends NodeBase { const isIIFE = this.parent.type === 'CallExpression' && (this.parent as CallExpression).callee === this; if ( - (isIIFE || this.getDeclarationVariable()?.onlyFunctionCallUsed) && + (isIIFE || this.getDeclarationVariable()?.getOnlyFunctionCallUsed()) && this.allArguments.length > 0 ) { this.applyFunctionParameterOptimization(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index bb9a8a52168..7b33996150a 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -4,9 +4,7 @@ import { INTERACTION_CALLED } from '../../NodeInteractions'; import type ChildScope from '../../scopes/ChildScope'; import FunctionScope from '../../scopes/FunctionScope'; import type { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import type Variable from '../../variables/Variable'; import type BlockStatement from '../BlockStatement'; -import type ExportDefaultDeclaration from '../ExportDefaultDeclaration'; import Identifier, { type IdentifierWithVariable } from '../Identifier'; import type { ExpressionEntity } from './Expression'; import { UNKNOWN_EXPRESSION } from './Expression'; @@ -92,15 +90,6 @@ export default class FunctionNode extends FunctionBase { return false; } - getDeclarationVariable(): Variable | null { - return ( - this.id?.variable ?? - (this.parent.type === 'ExportDefaultDeclaration' - ? (this.parent as ExportDefaultDeclaration).variable - : null) - ); - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { super.include(context, includeChildrenRecursively); this.id?.include(); diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 4c3b7ba8857..967964061f6 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -5,6 +5,7 @@ import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; import type CallExpression from '../nodes/CallExpression'; +import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; import type { NodeBase } from '../nodes/shared/Node'; @@ -36,13 +37,37 @@ export default class Variable extends ExpressionEntity { */ addReference(_identifier: Identifier): void {} - onlyFunctionCallUsed = true; + private usedTimes = 0; + private exportDefaultVariable: Variable | null = null; + private onlyFunctionCallUsed = true; + + /** + * Check if the identifier variable is only used as function call + * Forward the check to the export default variable if it is only used once + * @returns true if the variable is only used as function call + */ + + getOnlyFunctionCallUsed(): boolean { + if (this.usedTimes === 1 && this.exportDefaultVariable) + return this.exportDefaultVariable.getOnlyFunctionCallUsed(); + return this.onlyFunctionCallUsed; + } + + /** + * Collect the places where the identifier variable is used + * @param usedPlace Where the variable is used + */ addUsedPlace(usedPlace: NodeBase): void { + this.usedTimes++; const isFunctionCall = usedPlace.parent.type === 'CallExpression' && (usedPlace.parent as CallExpression).callee === usedPlace; if (!isFunctionCall) { this.onlyFunctionCallUsed = false; + const isExportDefaultVariable = usedPlace.parent.type === 'ExportDefaultDeclaration'; + if (isExportDefaultVariable) { + this.exportDefaultVariable = (usedPlace.parent as ExportDefaultDeclaration).variable; + } } } diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js index d2cc14422b7..39872a4cff7 100644 --- a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/_expected.js @@ -1,7 +1,14 @@ -function module2Func(enable) { +function importThenExportSub(enable) { { - return 'module2Func'; + return 'importThenExportSub'; } } -console.log(module2Func()); +function defineAndExportDefault(enable) { + { + return 'defineAndExportDefault'; + } +} + +console.log(importThenExportSub()); +console.log(defineAndExportDefault()); diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/define_and_export_default.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/define_and_export_default.js new file mode 100644 index 00000000000..b52e1846d0f --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/define_and_export_default.js @@ -0,0 +1,7 @@ +function defineAndExportDefault(enable) { + if (enable) { + return 'defineAndExportDefault'; + } +} + +export default defineAndExportDefault; diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export.js new file mode 100644 index 00000000000..30e7e2b911d --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export.js @@ -0,0 +1,3 @@ +import { importThenExportSub } from './import_then_export_sub.js' + +export { importThenExportSub } diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export_sub.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export_sub.js new file mode 100644 index 00000000000..2afc494681e --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/import_then_export_sub.js @@ -0,0 +1,5 @@ +export function importThenExportSub(enable) { + if (enable) { + return 'importThenExportSub'; + } +} diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js index 2d4ea2e4b51..ad0e9fb2639 100644 --- a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js +++ b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/main.js @@ -1,3 +1,5 @@ -import { module2Func } from './module1.js' +import { importThenExportSub } from './import_then_export.js' +import defineAndExportDefault from './define_and_export_default.js' -console.log(module2Func(true)) +console.log(importThenExportSub(true)) +console.log(defineAndExportDefault(true)) diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js deleted file mode 100644 index a90138720ac..00000000000 --- a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module1.js +++ /dev/null @@ -1,3 +0,0 @@ -import { module2Func } from './module2.js' - -export { module2Func } diff --git a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js b/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js deleted file mode 100644 index c66b12de017..00000000000 --- a/test/form/samples/tree-shake-literal-parameter/indirect-import-function/module2.js +++ /dev/null @@ -1,5 +0,0 @@ -export function module2Func(enable) { - if (enable) { - return 'module2Func'; - } -} From 4e71e666f1075c84d9c298e3bcab01fbcb375fd7 Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 20:00:59 +0800 Subject: [PATCH 30/49] performance --- src/ast/variables/ParameterVariable.ts | 5 ++++- src/ast/variables/Variable.ts | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index ef2c4cb4414..b648dd53045 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -83,6 +83,9 @@ export default class ParameterVariable extends LocalVariable { * @param value The known value of the parameter to be set. */ setKnownValue(value: ExpressionEntity | undefined): void { + if (this.isReassigned) { + return; + } if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -97,10 +100,10 @@ export default class ParameterVariable extends LocalVariable { recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - this.knownExpressionsToBeDeoptimized.push(origin); if (this.isReassigned) { return UnknownValue; } + this.knownExpressionsToBeDeoptimized.push(origin); if (this.knownValue) { return recursionTracker.withTrackedEntityAtPath( path, diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 967964061f6..d38946d54bb 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -64,8 +64,7 @@ export default class Variable extends ExpressionEntity { (usedPlace.parent as CallExpression).callee === usedPlace; if (!isFunctionCall) { this.onlyFunctionCallUsed = false; - const isExportDefaultVariable = usedPlace.parent.type === 'ExportDefaultDeclaration'; - if (isExportDefaultVariable) { + if (this.usedTimes == 1 && usedPlace.parent.type === 'ExportDefaultDeclaration') { this.exportDefaultVariable = (usedPlace.parent as ExportDefaultDeclaration).variable; } } From 35857ff7ecdc138a29551f16551f3f4e9f1ba6f0 Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 22:40:15 +0800 Subject: [PATCH 31/49] refactor: UNKNOWN_EXPRESSION --- src/ast/nodes/shared/FunctionBase.ts | 23 ++++++++--------------- src/ast/variables/ParameterVariable.ts | 25 +++++++++++-------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index a3276f1ecf8..f577905353a 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -37,9 +37,6 @@ import { import type { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; -const UnknownArgument = Symbol('Unknown Argument'); -type FunctionParameterState = ExpressionNode | SpreadElement | typeof UnknownArgument; - export default abstract class FunctionBase extends NodeBase { declare body: BlockStatement | ExpressionNode; declare params: PatternNode[]; @@ -70,13 +67,13 @@ export default abstract class FunctionBase extends NodeBase { this.flags = setFlag(this.flags, Flag.generator, value); } - private knownParameters: FunctionParameterState[] = []; - private allArguments: (ExpressionNode | SpreadElement)[][] = []; + private knownParameters: ExpressionEntity[] = []; + private allArguments: ExpressionEntity[][] = []; /** * updated knownParameters when a call is made to this function * @param newArguments arguments of the call */ - updateKnownArguments(newArguments: (SpreadElement | ExpressionNode)[]): void { + updateKnownArguments(newArguments: ExpressionEntity[]): void { for (let position = 0; position < this.params.length; position++) { const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; @@ -86,7 +83,7 @@ export default abstract class FunctionBase extends NodeBase { const knownParameter = this.knownParameters[position]; if (knownParameter === undefined) { this.knownParameters[position] = argument; - } else if (knownParameter !== UnknownArgument) { + } else if (knownParameter !== UNKNOWN_EXPRESSION) { // update knownParameter with argument if (knownParameter === argument) { continue; @@ -109,13 +106,13 @@ export default abstract class FunctionBase extends NodeBase { deoptimizeCache() {} }); if (knownLiteral !== newLiteral || typeof knownLiteral === 'symbol') { - this.knownParameters[position] = UnknownArgument; + this.knownParameters[position] = UNKNOWN_EXPRESSION; } // else both are the same literal, no need to update } } } - forwardArgumentsForFunctionCalledOnce(newArguments: (SpreadElement | ExpressionNode)[]): void { + forwardArgumentsForFunctionCalledOnce(newArguments: ExpressionEntity[]): void { for (let position = 0; position < this.params.length; position++) { const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; @@ -147,16 +144,12 @@ export default abstract class FunctionBase extends NodeBase { this.updateKnownArguments(argumentsList); } for (let position = 0; position < this.params.length; position++) { - const knownParameter = this.knownParameters[position]; + const knownParameter = this.knownParameters[position] ?? UNKNOWN_EXPRESSION; const parameter = this.params[position]; const ParameterVariable = parameter.variable as ParameterVariable | null; // Parameters without default values if (parameter instanceof Identifier) { - if (knownParameter === UnknownArgument) { - ParameterVariable?.setKnownValue(undefined); - } else { - ParameterVariable?.setKnownValue(knownParameter); - } + ParameterVariable?.setKnownValue(knownParameter); } } } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index b648dd53045..5be18075bda 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -75,14 +75,14 @@ export default class ParameterVariable extends LocalVariable { } } - knownValue: ExpressionEntity | undefined = undefined; + private knownValue: ExpressionEntity = UNKNOWN_EXPRESSION; /** * If we are sure about the value of this parameter, we can set it here. * It can be a literal or the only possible value of the parameter. * an undefined value means that the parameter is not known. * @param value The known value of the parameter to be set. */ - setKnownValue(value: ExpressionEntity | undefined): void { + setKnownValue(value: ExpressionEntity): void { if (this.isReassigned) { return; } @@ -104,15 +104,12 @@ export default class ParameterVariable extends LocalVariable { return UnknownValue; } this.knownExpressionsToBeDeoptimized.push(origin); - if (this.knownValue) { - return recursionTracker.withTrackedEntityAtPath( - path, - this.knownValue, - () => this.knownValue!.getLiteralValueAtPath(path, recursionTracker, origin), - UnknownValue - ); - } - return UnknownValue; + return recursionTracker.withTrackedEntityAtPath( + path, + this.knownValue, + () => this.knownValue!.getLiteralValueAtPath(path, recursionTracker, origin), + UnknownValue + ); } hasEffectsOnInteractionAtPath( @@ -121,9 +118,9 @@ export default class ParameterVariable extends LocalVariable { context: HasEffectsContext ): boolean { // assigned is a bit different, since the value has a new name (the parameter) - return this.knownValue && interaction.type !== INTERACTION_ASSIGNED - ? this.knownValue.hasEffectsOnInteractionAtPath(path, interaction, context) - : super.hasEffectsOnInteractionAtPath(path, interaction, context); + return interaction.type === INTERACTION_ASSIGNED + ? super.hasEffectsOnInteractionAtPath(path, interaction, context) + : this.knownValue.hasEffectsOnInteractionAtPath(path, interaction, context); } deoptimizeArgumentsOnInteractionAtPath(interaction: NodeInteraction, path: ObjectPath): void { From 4d415322b3afe24fd0a34d5556c3da55c84a63ad Mon Sep 17 00:00:00 2001 From: liuly Date: Sat, 30 Mar 2024 22:53:08 +0800 Subject: [PATCH 32/49] refactor: reduce complexity --- src/ast/nodes/shared/FunctionBase.ts | 44 +++++++++++++--------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index f577905353a..ebbf68a294a 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -80,35 +80,31 @@ export default abstract class FunctionBase extends NodeBase { if (!parameter || parameter instanceof RestElement) { break; } + const knownParameter = this.knownParameters[position]; if (knownParameter === undefined) { this.knownParameters[position] = argument; - } else if (knownParameter !== UNKNOWN_EXPRESSION) { - // update knownParameter with argument - if (knownParameter === argument) { - continue; - } - if ( - knownParameter instanceof Identifier && + continue; + } + if ( + knownParameter === UNKNOWN_EXPRESSION || + knownParameter === argument || + (knownParameter instanceof Identifier && argument instanceof Identifier && - knownParameter.variable === argument.variable - ) { - continue; - } - const knownLiteral = knownParameter.getLiteralValueAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - { - deoptimizeCache() {} - } - ); - const newLiteral = argument.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { - deoptimizeCache() {} - }); - if (knownLiteral !== newLiteral || typeof knownLiteral === 'symbol') { - this.knownParameters[position] = UNKNOWN_EXPRESSION; - } // else both are the same literal, no need to update + knownParameter.variable === argument.variable) + ) { + continue; } + + const oldValue = knownParameter.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { + deoptimizeCache() {} + }); + const newValue = argument.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { + deoptimizeCache() {} + }); + if (oldValue !== newValue || typeof oldValue === 'symbol') { + this.knownParameters[position] = UNKNOWN_EXPRESSION; + } // else both are the same literal, no need to update } } From de1a0ab2510d3a8a3e27864e4366bb556bdb0ac6 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 4 Apr 2024 09:22:07 +0800 Subject: [PATCH 33/49] fix: export default function foo and foo called from same mod --- src/ast/nodes/FunctionDeclaration.ts | 14 +++++++++++--- src/ast/nodes/shared/FunctionBase.ts | 21 +++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index 3a84691a86f..4492e3c112f 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -1,5 +1,5 @@ import type ChildScope from '../scopes/ChildScope'; -import type Variable from '../variables/Variable'; +import type ExportDefaultDeclaration from './ExportDefaultDeclaration'; import Identifier, { type IdentifierWithVariable } from './Identifier'; import type * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; @@ -15,8 +15,16 @@ export default class FunctionDeclaration extends FunctionNode { } } - getDeclarationVariable(): Variable | null { - return super.getDeclarationVariable() ?? this.id?.variable ?? null; + onlyFunctionCallUsed(): boolean { + let isOnlyFunctionCallUsed = true; + if (this.parent.type === 'ExportDefaultDeclaration') { + isOnlyFunctionCallUsed &&= ( + this.parent as ExportDefaultDeclaration + ).variable.getOnlyFunctionCallUsed(); + } + // if no id, it cannot be accessed from the same module + isOnlyFunctionCallUsed &&= this.id?.variable.getOnlyFunctionCallUsed() ?? true; + return isOnlyFunctionCallUsed; } parseNode(esTreeNode: GenericEsTreeNode): this { diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index ebbf68a294a..3baa96b98d4 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -276,30 +276,23 @@ export default abstract class FunctionBase extends NodeBase { } /** - * If the function is assigned/bound/... to some identifier declaration, - * getDeclarationVariable will return the Variable entity of that declaration. - * It can be used to track if all usages of this function are only function calls. - * While there are methods like deoptimizePath, - * this one can make sure 100% of usages are function calls. - * @returns the Variable entity of the declaration + * If the function (expression or declaration) is only used as function calls */ - getDeclarationVariable(): Variable | null { + onlyFunctionCallUsed(): boolean { + let variable: Variable | null = null; if (this.parent.type === 'VariableDeclarator') { - return (this.parent as VariableDeclarator).id.variable ?? null; + variable = (this.parent as VariableDeclarator).id.variable ?? null; } if (this.parent.type === 'ExportDefaultDeclaration') { - return (this.parent as ExportDefaultDeclaration).variable; + variable = (this.parent as ExportDefaultDeclaration).variable; } - return null; + return variable?.getOnlyFunctionCallUsed() ?? false; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { const isIIFE = this.parent.type === 'CallExpression' && (this.parent as CallExpression).callee === this; - if ( - (isIIFE || this.getDeclarationVariable()?.getOnlyFunctionCallUsed()) && - this.allArguments.length > 0 - ) { + if ((isIIFE || this.onlyFunctionCallUsed()) && this.allArguments.length > 0) { this.applyFunctionParameterOptimization(); } if (!this.deoptimized) this.applyDeoptimizations(); From d54864f45b44d68d6a7979852a46c119f0b084ba Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 4 Apr 2024 10:21:14 +0800 Subject: [PATCH 34/49] style: NodeType --- src/ast/nodes/FunctionDeclaration.ts | 4 ++-- src/ast/nodes/shared/FunctionBase.ts | 8 +++++--- src/ast/variables/Variable.ts | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index 4492e3c112f..6122094d030 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -1,7 +1,7 @@ import type ChildScope from '../scopes/ChildScope'; import type ExportDefaultDeclaration from './ExportDefaultDeclaration'; import Identifier, { type IdentifierWithVariable } from './Identifier'; -import type * as NodeType from './NodeType'; +import * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; import type { GenericEsTreeNode } from './shared/Node'; @@ -17,7 +17,7 @@ export default class FunctionDeclaration extends FunctionNode { onlyFunctionCallUsed(): boolean { let isOnlyFunctionCallUsed = true; - if (this.parent.type === 'ExportDefaultDeclaration') { + if (this.parent.type === NodeType.ExportDefaultDeclaration) { isOnlyFunctionCallUsed &&= ( this.parent as ExportDefaultDeclaration ).variable.getOnlyFunctionCallUsed(); diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 3baa96b98d4..dea5e60200f 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -22,6 +22,7 @@ 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'; @@ -280,10 +281,10 @@ export default abstract class FunctionBase extends NodeBase { */ onlyFunctionCallUsed(): boolean { let variable: Variable | null = null; - if (this.parent.type === 'VariableDeclarator') { + if (this.parent.type === NodeType.VariableDeclarator) { variable = (this.parent as VariableDeclarator).id.variable ?? null; } - if (this.parent.type === 'ExportDefaultDeclaration') { + if (this.parent.type === NodeType.ExportDefaultDeclaration) { variable = (this.parent as ExportDefaultDeclaration).variable; } return variable?.getOnlyFunctionCallUsed() ?? false; @@ -291,7 +292,8 @@ export default abstract class FunctionBase extends NodeBase { include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { const isIIFE = - this.parent.type === 'CallExpression' && (this.parent as CallExpression).callee === this; + this.parent.type === NodeType.CallExpression && + (this.parent as CallExpression).callee === this; if ((isIIFE || this.onlyFunctionCallUsed()) && this.allArguments.length > 0) { this.applyFunctionParameterOptimization(); } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index d38946d54bb..5f8b1d39d0f 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -7,6 +7,7 @@ import { INTERACTION_ACCESSED } from '../NodeInteractions'; import type CallExpression from '../nodes/CallExpression'; import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; +import * as NodeType from '../nodes/NodeType'; import { ExpressionEntity } from '../nodes/shared/Expression'; import type { NodeBase } from '../nodes/shared/Node'; import type { VariableKind } from '../nodes/shared/VariableKinds'; @@ -60,11 +61,11 @@ export default class Variable extends ExpressionEntity { addUsedPlace(usedPlace: NodeBase): void { this.usedTimes++; const isFunctionCall = - usedPlace.parent.type === 'CallExpression' && + usedPlace.parent.type === NodeType.CallExpression && (usedPlace.parent as CallExpression).callee === usedPlace; if (!isFunctionCall) { this.onlyFunctionCallUsed = false; - if (this.usedTimes == 1 && usedPlace.parent.type === 'ExportDefaultDeclaration') { + if (this.usedTimes == 1 && usedPlace.parent.type === NodeType.ExportDefaultDeclaration) { this.exportDefaultVariable = (usedPlace.parent as ExportDefaultDeclaration).variable; } } From 2b19f6278195c4e5fb55612dd9600d492df9b812 Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 5 Apr 2024 09:19:28 +0800 Subject: [PATCH 35/49] style: remove counter --- src/ast/variables/Variable.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 5f8b1d39d0f..7bf80751607 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -38,20 +38,18 @@ export default class Variable extends ExpressionEntity { */ addReference(_identifier: Identifier): void {} - private usedTimes = 0; - private exportDefaultVariable: Variable | null = null; + private exportDefaultVariables: Variable[] = []; private onlyFunctionCallUsed = true; - /** * Check if the identifier variable is only used as function call * Forward the check to the export default variable if it is only used once * @returns true if the variable is only used as function call */ - getOnlyFunctionCallUsed(): boolean { - if (this.usedTimes === 1 && this.exportDefaultVariable) - return this.exportDefaultVariable.getOnlyFunctionCallUsed(); - return this.onlyFunctionCallUsed; + return ( + this.onlyFunctionCallUsed && + this.exportDefaultVariables.every(variable => variable.getOnlyFunctionCallUsed()) + ); } /** @@ -59,14 +57,14 @@ export default class Variable extends ExpressionEntity { * @param usedPlace Where the variable is used */ addUsedPlace(usedPlace: NodeBase): void { - this.usedTimes++; const isFunctionCall = usedPlace.parent.type === NodeType.CallExpression && (usedPlace.parent as CallExpression).callee === usedPlace; if (!isFunctionCall) { - this.onlyFunctionCallUsed = false; - if (this.usedTimes == 1 && usedPlace.parent.type === NodeType.ExportDefaultDeclaration) { - this.exportDefaultVariable = (usedPlace.parent as ExportDefaultDeclaration).variable; + if (usedPlace.parent.type === NodeType.ExportDefaultDeclaration) { + this.exportDefaultVariables.push((usedPlace.parent as ExportDefaultDeclaration).variable); + } else { + this.onlyFunctionCallUsed = false; } } } From 7d6f5fb60a1adcaa82a7b22d1b0d2c3a9835ba3c Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 5 Apr 2024 15:41:52 +0800 Subject: [PATCH 36/49] perf: cache onlyfunctioncall result --- src/ast/variables/Variable.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 7bf80751607..a39be495c5d 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,5 +1,6 @@ import type ExternalModule from '../../ExternalModule'; import type Module from '../../Module'; +import { EMPTY_ARRAY } from '../../utils/blank'; import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; @@ -40,16 +41,21 @@ export default class Variable extends ExpressionEntity { private exportDefaultVariables: Variable[] = []; private onlyFunctionCallUsed = true; + private isOnlyFunctionCallUsedAnalysed = false; /** * Check if the identifier variable is only used as function call * Forward the check to the export default variable if it is only used once * @returns true if the variable is only used as function call */ getOnlyFunctionCallUsed(): boolean { - return ( - this.onlyFunctionCallUsed && - this.exportDefaultVariables.every(variable => variable.getOnlyFunctionCallUsed()) - ); + if (!this.isOnlyFunctionCallUsedAnalysed) { + this.isOnlyFunctionCallUsedAnalysed = true; + this.onlyFunctionCallUsed &&= this.exportDefaultVariables.every(variable => + variable.getOnlyFunctionCallUsed() + ); + this.exportDefaultVariables = EMPTY_ARRAY as unknown as Variable[]; + } + return this.onlyFunctionCallUsed; } /** From e13a5143e7103b5fd3f9c3bc0b46c6ea872bcdbb Mon Sep 17 00:00:00 2001 From: liuly Date: Sun, 7 Apr 2024 20:50:36 +0800 Subject: [PATCH 37/49] style&perf: remove args slice --- src/ast/nodes/shared/FunctionBase.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index aa8eeb0d9d8..44f2d83ad4d 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -38,6 +38,8 @@ import { import type { ObjectEntity } from './ObjectEntity'; import type { PatternNode } from './Pattern'; +type InteractionCalledArguments = NodeInteractionCalled['args']; + export default abstract class FunctionBase extends NodeBase { declare body: BlockStatement | ExpressionNode; declare params: PatternNode[]; @@ -69,14 +71,15 @@ export default abstract class FunctionBase extends NodeBase { } private knownParameters: ExpressionEntity[] = []; - private allArguments: ExpressionEntity[][] = []; + private allArguments: InteractionCalledArguments[] = []; /** * updated knownParameters when a call is made to this function * @param newArguments arguments of the call */ - updateKnownArguments(newArguments: ExpressionEntity[]): void { + updateKnownArguments(newArguments: InteractionCalledArguments): void { for (let position = 0; position < this.params.length; position++) { - const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; + // only the "this" argument newArguments[0] can be null + const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; if (!parameter || parameter instanceof RestElement) { break; @@ -109,9 +112,10 @@ export default abstract class FunctionBase extends NodeBase { } } - forwardArgumentsForFunctionCalledOnce(newArguments: ExpressionEntity[]): void { + forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void { for (let position = 0; position < this.params.length; position++) { - const argument = newArguments[position] ?? UNDEFINED_EXPRESSION; + // only the "this" argument newArguments[0] can be null + const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; if (!parameter || parameter instanceof RestElement) { break; @@ -178,7 +182,7 @@ export default abstract class FunctionBase extends NodeBase { this.addArgumentToBeDeoptimized(argument); } } - this.allArguments.push(args.slice(1) as (ExpressionNode | SpreadElement)[]); + this.allArguments.push(args); } else { this.getObjectEntity().deoptimizeArgumentsOnInteractionAtPath( interaction, From 7d54f27be65a6411a725f2862cc8deba1784508e Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 8 Apr 2024 09:43:35 +0800 Subject: [PATCH 38/49] perf: export default variable --- src/ast/variables/ExportDefaultVariable.ts | 25 +++++++++++++++++++++- src/ast/variables/Variable.ts | 19 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/ast/variables/ExportDefaultVariable.ts b/src/ast/variables/ExportDefaultVariable.ts index 979405b5784..2a8e92d0512 100644 --- a/src/ast/variables/ExportDefaultVariable.ts +++ b/src/ast/variables/ExportDefaultVariable.ts @@ -3,10 +3,16 @@ 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'; +enum OriginalVariableStage { + BIND_REFERENCE, + GENERATE_CHUNK +} + export default class ExportDefaultVariable extends LocalVariable { hasId = false; @@ -37,6 +43,15 @@ export default class ExportDefaultVariable extends LocalVariable { } } + addUsedPlace(usedPlace: NodeBase): void { + const original = this.getOriginalVariable(OriginalVariableStage.BIND_REFERENCE); + if (original === this) { + super.addUsedPlace(usedPlace); + } else { + original.addUsedPlace(usedPlace); + } + } + forbidName(name: string) { const original = this.getOriginalVariable(); if (original === this) { @@ -57,6 +72,7 @@ export default class ExportDefaultVariable extends LocalVariable { getDirectOriginalVariable(): Variable | null { return this.originalId && + this.originalId.variable && (this.hasId || !( this.originalId.isPossibleTDZ() || @@ -76,7 +92,14 @@ export default class ExportDefaultVariable extends LocalVariable { : original.getName(getPropertyAccess); } - getOriginalVariable(): Variable { + private previousOriginalVariableState: OriginalVariableStage | null = null; + getOriginalVariable( + originalVariableState: OriginalVariableStage = OriginalVariableStage.GENERATE_CHUNK + ): Variable { + if (this.previousOriginalVariableState !== originalVariableState) { + this.originalVariable = null; + this.previousOriginalVariableState = originalVariableState; + } if (this.originalVariable) return this.originalVariable; // eslint-disable-next-line @typescript-eslint/no-this-alias let original: Variable | null = this; diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index a39be495c5d..02988d448f6 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,12 +1,10 @@ import type ExternalModule from '../../ExternalModule'; import type Module from '../../Module'; -import { EMPTY_ARRAY } from '../../utils/blank'; import type { RenderOptions } from '../../utils/renderHelpers'; import type { HasEffectsContext } from '../ExecutionContext'; import type { NodeInteraction } from '../NodeInteractions'; import { INTERACTION_ACCESSED } from '../NodeInteractions'; import type CallExpression from '../nodes/CallExpression'; -import type ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import type Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; import { ExpressionEntity } from '../nodes/shared/Expression'; @@ -39,22 +37,13 @@ export default class Variable extends ExpressionEntity { */ addReference(_identifier: Identifier): void {} - private exportDefaultVariables: Variable[] = []; private onlyFunctionCallUsed = true; - private isOnlyFunctionCallUsedAnalysed = false; /** * Check if the identifier variable is only used as function call * Forward the check to the export default variable if it is only used once * @returns true if the variable is only used as function call */ getOnlyFunctionCallUsed(): boolean { - if (!this.isOnlyFunctionCallUsedAnalysed) { - this.isOnlyFunctionCallUsedAnalysed = true; - this.onlyFunctionCallUsed &&= this.exportDefaultVariables.every(variable => - variable.getOnlyFunctionCallUsed() - ); - this.exportDefaultVariables = EMPTY_ARRAY as unknown as Variable[]; - } return this.onlyFunctionCallUsed; } @@ -66,12 +55,8 @@ export default class Variable extends ExpressionEntity { const isFunctionCall = usedPlace.parent.type === NodeType.CallExpression && (usedPlace.parent as CallExpression).callee === usedPlace; - if (!isFunctionCall) { - if (usedPlace.parent.type === NodeType.ExportDefaultDeclaration) { - this.exportDefaultVariables.push((usedPlace.parent as ExportDefaultDeclaration).variable); - } else { - this.onlyFunctionCallUsed = false; - } + if (!isFunctionCall && usedPlace.parent.type !== NodeType.ExportDefaultDeclaration) { + this.onlyFunctionCallUsed = false; } } From a8d2f86f3129e4c11f228a8d2cc43490571c12c0 Mon Sep 17 00:00:00 2001 From: liuly Date: Mon, 8 Apr 2024 15:06:17 +0800 Subject: [PATCH 39/49] perf: export default variable --- src/ast/nodes/FunctionDeclaration.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index 6122094d030..1b7079be592 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -1,7 +1,6 @@ import type ChildScope from '../scopes/ChildScope'; -import type ExportDefaultDeclaration from './ExportDefaultDeclaration'; import Identifier, { type IdentifierWithVariable } from './Identifier'; -import * as NodeType from './NodeType'; +import type * as NodeType from './NodeType'; import FunctionNode from './shared/FunctionNode'; import type { GenericEsTreeNode } from './shared/Node'; @@ -16,15 +15,8 @@ export default class FunctionDeclaration extends FunctionNode { } onlyFunctionCallUsed(): boolean { - let isOnlyFunctionCallUsed = true; - if (this.parent.type === NodeType.ExportDefaultDeclaration) { - isOnlyFunctionCallUsed &&= ( - this.parent as ExportDefaultDeclaration - ).variable.getOnlyFunctionCallUsed(); - } - // if no id, it cannot be accessed from the same module - isOnlyFunctionCallUsed &&= this.id?.variable.getOnlyFunctionCallUsed() ?? true; - return isOnlyFunctionCallUsed; + // call super.onlyFunctionCallUsed for export default anonymous function + return this.id?.variable.getOnlyFunctionCallUsed() ?? super.onlyFunctionCallUsed(); } parseNode(esTreeNode: GenericEsTreeNode): this { From 97fcfad4218c437d13208cf90d4505a480a6a8a8 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 15:12:35 +0800 Subject: [PATCH 40/49] style: small updates: naming, private... --- src/ast/nodes/shared/CallExpressionBase.ts | 4 +- src/ast/nodes/shared/FunctionBase.ts | 60 +++++++++++++--------- src/ast/variables/ExportDefaultVariable.ts | 3 ++ src/ast/variables/LocalVariable.ts | 3 +- 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/ast/nodes/shared/CallExpressionBase.ts b/src/ast/nodes/shared/CallExpressionBase.ts index 392424930f0..04c045b5d93 100644 --- a/src/ast/nodes/shared/CallExpressionBase.ts +++ b/src/ast/nodes/shared/CallExpressionBase.ts @@ -1,4 +1,4 @@ -import { EMPTY_SET } from '../../../utils/blank'; +import { EMPTY_ARRAY, EMPTY_SET } from '../../../utils/blank'; import type { DeoptimizableEntity } from '../../DeoptimizableEntity'; import type { HasEffectsContext } from '../../ExecutionContext'; import type { NodeInteraction, NodeInteractionCalled } from '../../NodeInteractions'; @@ -59,7 +59,7 @@ export default abstract class CallExpressionBase extends NodeBase implements Deo this.returnExpression = UNKNOWN_RETURN_EXPRESSION; const { deoptimizableDependentExpressions, expressionsToBeDeoptimized } = this; this.expressionsToBeDeoptimized = EMPTY_SET; - this.deoptimizableDependentExpressions = []; + this.deoptimizableDependentExpressions = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; for (const expression of deoptimizableDependentExpressions) { expression.deoptimizeCache(); } diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 44f2d83ad4d..824ae471d22 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -40,6 +40,11 @@ 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[]; @@ -70,49 +75,54 @@ export default abstract class FunctionBase extends NodeBase { this.flags = setFlag(this.flags, Flag.generator, value); } - private knownParameters: ExpressionEntity[] = []; + private knownParameterValues: ExpressionEntity[] = []; private allArguments: InteractionCalledArguments[] = []; /** - * updated knownParameters when a call is made to this function + * update knownParameterValues when a call is made to this function * @param newArguments arguments of the call */ - updateKnownArguments(newArguments: InteractionCalledArguments): void { + 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]; if (!parameter || parameter instanceof RestElement) { break; } - const knownParameter = this.knownParameters[position]; - if (knownParameter === undefined) { - this.knownParameters[position] = argument; + const knownParameterValue = this.knownParameterValues[position]; + if (knownParameterValue === undefined) { + this.knownParameterValues[position] = argument; continue; } if ( - knownParameter === UNKNOWN_EXPRESSION || - knownParameter === argument || - (knownParameter instanceof Identifier && + knownParameterValue === UNKNOWN_EXPRESSION || + knownParameterValue === argument || + (knownParameterValue instanceof Identifier && argument instanceof Identifier && - knownParameter.variable === argument.variable) + knownParameterValue.variable === argument.variable) ) { continue; } - const oldValue = knownParameter.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { - deoptimizeCache() {} - }); - const newValue = argument.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, { - deoptimizeCache() {} - }); + 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.knownParameters[position] = UNKNOWN_EXPRESSION; + this.knownParameterValues[position] = UNKNOWN_EXPRESSION; } // else both are the same literal, no need to update } } - forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void { + private forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void { for (let position = 0; position < this.params.length; position++) { // only the "this" argument newArguments[0] can be null const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; @@ -133,24 +143,24 @@ export default abstract class FunctionBase extends NodeBase { * `undefined` (no call is made) -> an expression -> `UnknownArgument` * we are sure it will converge, and can use state from last iteration */ - applyFunctionParameterOptimization() { + private applyFunctionParameterOptimization() { if (this.allArguments.length === 1) { - // we are sure what knownParameters will be, so skip it and do setKnownValue + // 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.updateKnownArguments(argumentsList); + this.updateKnownParameterValues(argumentsList); } for (let position = 0; position < this.params.length; position++) { - const knownParameter = this.knownParameters[position] ?? UNKNOWN_EXPRESSION; const parameter = this.params[position]; - const ParameterVariable = parameter.variable as ParameterVariable | null; // Parameters without default values if (parameter instanceof Identifier) { - ParameterVariable?.setKnownValue(knownParameter); + const parameterVariable = parameter.variable as ParameterVariable | null; + const knownParameter = this.knownParameterValues[position] ?? UNKNOWN_EXPRESSION; + parameterVariable?.setKnownValue(knownParameter); } } } @@ -283,7 +293,7 @@ export default abstract class FunctionBase extends NodeBase { /** * If the function (expression or declaration) is only used as function calls */ - onlyFunctionCallUsed(): boolean { + protected onlyFunctionCallUsed(): boolean { let variable: Variable | null = null; if (this.parent.type === NodeType.VariableDeclarator) { variable = (this.parent as VariableDeclarator).id.variable ?? null; diff --git a/src/ast/variables/ExportDefaultVariable.ts b/src/ast/variables/ExportDefaultVariable.ts index 2a8e92d0512..748467d83a8 100644 --- a/src/ast/variables/ExportDefaultVariable.ts +++ b/src/ast/variables/ExportDefaultVariable.ts @@ -8,6 +8,8 @@ import LocalVariable from './LocalVariable'; import UndefinedVariable from './UndefinedVariable'; import type Variable from './Variable'; +// Between two stages, there might be variable bound change, like `export default a; a = 0`. +// Therefore we clear the cache if the stage changes. enum OriginalVariableStage { BIND_REFERENCE, GENERATE_CHUNK @@ -97,6 +99,7 @@ export default class ExportDefaultVariable extends LocalVariable { originalVariableState: OriginalVariableStage = OriginalVariableStage.GENERATE_CHUNK ): Variable { if (this.previousOriginalVariableState !== originalVariableState) { + // clear the `originalVariable` cache this.originalVariable = null; this.previousOriginalVariableState = originalVariableState; } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 8daff1a34c8..fe5bd488492 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -1,4 +1,5 @@ import type { AstContext, default as Module } from '../../Module'; +import { EMPTY_ARRAY } from '../../utils/blank'; import type { DeoptimizableEntity } from '../DeoptimizableEntity'; import type { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { createInclusionContext } from '../ExecutionContext'; @@ -92,7 +93,7 @@ export default class LocalVariable extends Variable { if (path.length === 0) { this.isReassigned = true; const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; + this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } From dfe4cd36e3ef6b552479cbec394d1118c7122d0a Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 15:24:18 +0800 Subject: [PATCH 41/49] perf: LogicalExpression deoptimize cache --- src/ast/nodes/LogicalExpression.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 34ce063059d..1b934295122 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -58,17 +58,19 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable deoptimizeCache(): void { this.isBranchResolutionAnalysed = false; - const { - scope: { context }, - expressionsToBeDeoptimized - } = this; - this.expressionsToBeDeoptimized = []; - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); + if (this.expressionsToBeDeoptimized.length > 0) { + const { + scope: { context }, + expressionsToBeDeoptimized + } = this; + 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(); } - // 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; From aa8741a88f14ed37f5eeef84a47d2a71d60f14ea Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 19:58:29 +0800 Subject: [PATCH 42/49] style: remove a condition which is always true --- src/ast/nodes/shared/FunctionBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 824ae471d22..8a25ee311e2 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -87,7 +87,7 @@ export default abstract class FunctionBase extends NodeBase { // 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]; - if (!parameter || parameter instanceof RestElement) { + if (parameter instanceof RestElement) { break; } From 40f9427a1d96f55f0cb98cd97097471632d42532 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 20:15:01 +0800 Subject: [PATCH 43/49] style: add protected --- src/ast/nodes/FunctionDeclaration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/nodes/FunctionDeclaration.ts b/src/ast/nodes/FunctionDeclaration.ts index 1b7079be592..9cfa8b21bd5 100644 --- a/src/ast/nodes/FunctionDeclaration.ts +++ b/src/ast/nodes/FunctionDeclaration.ts @@ -14,7 +14,7 @@ export default class FunctionDeclaration extends FunctionNode { } } - onlyFunctionCallUsed(): boolean { + protected onlyFunctionCallUsed(): boolean { // call super.onlyFunctionCallUsed for export default anonymous function return this.id?.variable.getOnlyFunctionCallUsed() ?? super.onlyFunctionCallUsed(); } From e02ab0dae2b252321fbf1d881bb7f3c94b941421 Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 20:24:46 +0800 Subject: [PATCH 44/49] style: remove a condition which is always true --- src/ast/nodes/shared/FunctionBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 8a25ee311e2..d7b2eea4741 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -127,7 +127,7 @@ export default abstract class FunctionBase extends NodeBase { // only the "this" argument newArguments[0] can be null const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; - if (!parameter || parameter instanceof RestElement) { + if (parameter instanceof RestElement) { break; } if (parameter instanceof Identifier) { From 51ffb9b89880906e2063ff10ad0f4073ef89fdad Mon Sep 17 00:00:00 2001 From: liuly Date: Thu, 18 Apr 2024 20:41:54 +0800 Subject: [PATCH 45/49] style: remove a condition --- src/ast/nodes/shared/FunctionBase.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index d7b2eea4741..5b986242cbb 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -124,14 +124,10 @@ export default abstract class FunctionBase extends NodeBase { private forwardArgumentsForFunctionCalledOnce(newArguments: InteractionCalledArguments): void { for (let position = 0; position < this.params.length; position++) { - // only the "this" argument newArguments[0] can be null - const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; const parameter = this.params[position]; - if (parameter instanceof RestElement) { - break; - } if (parameter instanceof Identifier) { const ParameterVariable = parameter.variable as ParameterVariable | null; + const argument = newArguments[position + 1] ?? UNDEFINED_EXPRESSION; ParameterVariable?.setKnownValue(argument); } } @@ -159,8 +155,8 @@ export default abstract class FunctionBase extends NodeBase { // Parameters without default values if (parameter instanceof Identifier) { const parameterVariable = parameter.variable as ParameterVariable | null; - const knownParameter = this.knownParameterValues[position] ?? UNKNOWN_EXPRESSION; - parameterVariable?.setKnownValue(knownParameter); + const knownParameterValue = this.knownParameterValues[position] ?? UNKNOWN_EXPRESSION; + parameterVariable?.setKnownValue(knownParameterValue); } } } From a559d2d17c93a0b3c84614d5022cf242c516901e Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 19 Apr 2024 18:56:03 +0800 Subject: [PATCH 46/49] refactor: lazy bind variable --- src/ast/nodes/Identifier.ts | 6 +++++- src/ast/nodes/MemberExpression.ts | 4 +++- src/ast/nodes/shared/FunctionBase.ts | 18 ++++++++++++++++- src/ast/variables/ExportDefaultVariable.ts | 20 ++----------------- src/ast/variables/ParameterVariable.ts | 3 --- .../do-not-optimize/_config.js | 3 +++ .../do-not-optimize/_expected.js | 18 +++++++++++++++++ .../do-not-optimize/main.js | 16 +++++++++++++++ 8 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 test/form/samples/tree-shake-literal-parameter/do-not-optimize/_config.js create mode 100644 test/form/samples/tree-shake-literal-parameter/do-not-optimize/_expected.js create mode 100644 test/form/samples/tree-shake-literal-parameter/do-not-optimize/main.js diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 6f342044dcf..8b13ea5b619 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -60,11 +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.variable.addUsedPlace(this); + this.isReferenceVariable = true; } } @@ -296,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 { diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index bc8453578de..cb2bf1d3b1e 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -156,7 +156,6 @@ export default class MemberExpression this.isUndefined = true; } else { this.variable = resolvedVariable; - this.variable.addUsedPlace(this); this.scope.addNamespaceMemberAccess(getStringFromPath(path!), resolvedVariable); } } else { @@ -397,6 +396,9 @@ export default class MemberExpression ); this.scope.context.requestTreeshakingPass(); } + if (this.variable) { + this.variable.addUsedPlace(this); + } } private applyAssignmentDeoptimization(): void { diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 5b986242cbb..1a38af48093 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -161,6 +161,15 @@ export default abstract class FunctionBase extends NodeBase { } } + private deoptimizeFunctionParameters() { + for (const parameter of this.params) { + if (parameter instanceof Identifier) { + const parameterVariable = parameter.variable as ParameterVariable | null; + parameterVariable?.setKnownValue(UNKNOWN_EXPRESSION); + } + } + } + protected objectEntity: ObjectEntity | null = null; deoptimizeArgumentsOnInteractionAtPath( @@ -300,13 +309,20 @@ export default abstract class FunctionBase extends NodeBase { 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; - if ((isIIFE || this.onlyFunctionCallUsed()) && this.allArguments.length > 0) { + const shoulOptimizeFunctionParameters = + (isIIFE || this.onlyFunctionCallUsed()) && this.allArguments.length > 0; + if (shoulOptimizeFunctionParameters) { this.applyFunctionParameterOptimization(); + } else if (this.functionParametersOptimized) { + this.deoptimizeFunctionParameters(); } + this.functionParametersOptimized = shoulOptimizeFunctionParameters; + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; const { brokenFlow } = context; diff --git a/src/ast/variables/ExportDefaultVariable.ts b/src/ast/variables/ExportDefaultVariable.ts index 748467d83a8..70810936e5a 100644 --- a/src/ast/variables/ExportDefaultVariable.ts +++ b/src/ast/variables/ExportDefaultVariable.ts @@ -8,13 +8,6 @@ import LocalVariable from './LocalVariable'; import UndefinedVariable from './UndefinedVariable'; import type Variable from './Variable'; -// Between two stages, there might be variable bound change, like `export default a; a = 0`. -// Therefore we clear the cache if the stage changes. -enum OriginalVariableStage { - BIND_REFERENCE, - GENERATE_CHUNK -} - export default class ExportDefaultVariable extends LocalVariable { hasId = false; @@ -46,7 +39,7 @@ export default class ExportDefaultVariable extends LocalVariable { } addUsedPlace(usedPlace: NodeBase): void { - const original = this.getOriginalVariable(OriginalVariableStage.BIND_REFERENCE); + const original = this.getOriginalVariable(); if (original === this) { super.addUsedPlace(usedPlace); } else { @@ -74,7 +67,6 @@ export default class ExportDefaultVariable extends LocalVariable { getDirectOriginalVariable(): Variable | null { return this.originalId && - this.originalId.variable && (this.hasId || !( this.originalId.isPossibleTDZ() || @@ -94,15 +86,7 @@ export default class ExportDefaultVariable extends LocalVariable { : original.getName(getPropertyAccess); } - private previousOriginalVariableState: OriginalVariableStage | null = null; - getOriginalVariable( - originalVariableState: OriginalVariableStage = OriginalVariableStage.GENERATE_CHUNK - ): Variable { - if (this.previousOriginalVariableState !== originalVariableState) { - // clear the `originalVariable` cache - this.originalVariable = null; - this.previousOriginalVariableState = originalVariableState; - } + getOriginalVariable(): Variable { if (this.originalVariable) return this.originalVariable; // eslint-disable-next-line @typescript-eslint/no-this-alias let original: Variable | null = this; diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 5be18075bda..17ba50db3d8 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -83,9 +83,6 @@ export default class ParameterVariable extends LocalVariable { * @param value The known value of the parameter to be set. */ setKnownValue(value: ExpressionEntity): void { - if (this.isReassigned) { - return; - } if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); diff --git a/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_config.js b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_config.js new file mode 100644 index 00000000000..d16519b0931 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'do not tree-shake literal parameter cases' +}); diff --git a/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_expected.js b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_expected.js new file mode 100644 index 00000000000..8e1230c7540 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/_expected.js @@ -0,0 +1,18 @@ +function foo(enable) { + console.log(enable ? 1 : 0); +} + +foo(1); + +function bar(f = foo) { + f(0); +} + +// global variable +g = function (enable) { + console.log(enable ? 1: 0); +}; + +g(1); + +export { bar }; diff --git a/test/form/samples/tree-shake-literal-parameter/do-not-optimize/main.js b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/main.js new file mode 100644 index 00000000000..079d60a1be5 --- /dev/null +++ b/test/form/samples/tree-shake-literal-parameter/do-not-optimize/main.js @@ -0,0 +1,16 @@ +function foo(enable) { + console.log(enable ? 1 : 0); +} + +foo(1); + +export function bar(f = foo) { + f(0); +} + +// global variable +g = function (enable) { + console.log(enable ? 1: 0); +}; + +g(1); From f73a3657e78b5c651454994b2a36e6d7016b3aca Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 19 Apr 2024 21:46:14 +0800 Subject: [PATCH 47/49] fix: refresh cache if isReassigned change for ParameterVariable --- src/ast/nodes/UpdateExpression.ts | 2 +- src/ast/nodes/shared/FunctionBase.ts | 2 +- src/ast/variables/GlobalVariable.ts | 9 ++++++--- src/ast/variables/LocalVariable.ts | 4 ++-- src/ast/variables/ParameterVariable.ts | 20 ++++++++++++++++++- src/ast/variables/Variable.ts | 6 +++++- .../samples/reassigned-parameter/_config.js | 3 +++ .../samples/reassigned-parameter/main.js | 7 +++++++ 8 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 test/function/samples/reassigned-parameter/_config.js create mode 100644 test/function/samples/reassigned-parameter/main.js diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 2d7b0613fe5..860bdd927a5 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -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(); } diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 1a38af48093..882647b2e9e 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -216,7 +216,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.isReassigned = true; + parameter.markReassigned(); } } } diff --git a/src/ast/variables/GlobalVariable.ts b/src/ast/variables/GlobalVariable.ts index d55ed63ae00..4f168ac8f86 100644 --- a/src/ast/variables/GlobalVariable.ts +++ b/src/ast/variables/GlobalVariable.ts @@ -13,9 +13,12 @@ import type { ObjectPath, PathTracker } from '../utils/PathTracker'; import Variable from './Variable'; export default class GlobalVariable extends Variable { - // Ensure we use live-bindings for globals as we do not know if they have - // been reassigned - isReassigned = true; + constructor(name: string) { + super(name); + // Ensure we use live-bindings for globals as we do not know if they have + // been reassigned + this.markReassigned(); + } deoptimizeArgumentsOnInteractionAtPath( interaction: NodeInteraction, diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index fe5bd488492..83b32a81ef7 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -91,7 +91,7 @@ export default class LocalVariable extends Variable { return; } if (path.length === 0) { - this.isReassigned = true; + this.markReassigned(); const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; this.expressionsToBeDeoptimized = EMPTY_ARRAY as unknown as DeoptimizableEntity[]; for (const expression of expressionsToBeDeoptimized) { @@ -222,7 +222,7 @@ export default class LocalVariable extends Variable { if (this.additionalInitializers === null) { this.additionalInitializers = [this.init]; this.init = UNKNOWN_EXPRESSION; - this.isReassigned = true; + this.markReassigned(); } return this.additionalInitializers; } diff --git a/src/ast/variables/ParameterVariable.ts b/src/ast/variables/ParameterVariable.ts index 17ba50db3d8..a411885a811 100644 --- a/src/ast/variables/ParameterVariable.ts +++ b/src/ast/variables/ParameterVariable.ts @@ -75,6 +75,17 @@ export default class ParameterVariable extends LocalVariable { } } + markReassigned(): void { + if (this.isReassigned) { + return; + } + super.markReassigned(); + for (const expression of this.knownExpressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + this.knownExpressionsToBeDeoptimized = []; + } + private knownValue: ExpressionEntity = UNKNOWN_EXPRESSION; /** * If we are sure about the value of this parameter, we can set it here. @@ -83,6 +94,9 @@ export default class ParameterVariable extends LocalVariable { * @param value The known value of the parameter to be set. */ setKnownValue(value: ExpressionEntity): void { + if (this.isReassigned) { + return; + } if (this.knownValue !== value) { for (const expression of this.knownExpressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -147,7 +161,11 @@ export default class ParameterVariable extends LocalVariable { } deoptimizePath(path: ObjectPath): void { - if (path.length === 0 || this.deoptimizedFields.has(UnknownKey)) { + if (this.deoptimizedFields.has(UnknownKey)) { + return; + } + if (path.length === 0) { + this.markReassigned(); return; } const key = path[0]; diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 02988d448f6..e22fe48b04c 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -19,7 +19,6 @@ export default class Variable extends ExpressionEntity { isId = false; // both NamespaceVariable and ExternalVariable can be namespaces declare isNamespace?: boolean; - isReassigned = false; kind: VariableKind | null = null; declare module?: Module | ExternalModule; renderBaseName: string | null = null; @@ -27,6 +26,11 @@ export default class Variable extends ExpressionEntity { private renderedLikeHoisted?: Variable; + readonly isReassigned = false; + markReassigned() { + (this as { isReassigned: boolean }).isReassigned = true; + } + constructor(public name: string) { super(); } diff --git a/test/function/samples/reassigned-parameter/_config.js b/test/function/samples/reassigned-parameter/_config.js new file mode 100644 index 00000000000..5923f0afe5d --- /dev/null +++ b/test/function/samples/reassigned-parameter/_config.js @@ -0,0 +1,3 @@ +module.exports = defineTest({ + description: 'parameters reassigned/updated should be detected' +}); diff --git a/test/function/samples/reassigned-parameter/main.js b/test/function/samples/reassigned-parameter/main.js new file mode 100644 index 00000000000..6210a58ea26 --- /dev/null +++ b/test/function/samples/reassigned-parameter/main.js @@ -0,0 +1,7 @@ +function f(a) { + assert.equal(a ? 'OK' : 'FAIL', 'OK'); + a = false; + assert.equal(a ? 'FAIL' : 'OK', 'OK'); +} + +f(true) From ea7ffab19c74a8a9d6848f04538f0adf66faddf6 Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 19 Apr 2024 22:09:07 +0800 Subject: [PATCH 48/49] fix: make sure deoptimize give a final state --- src/ast/nodes/shared/FunctionBase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 882647b2e9e..83f19f72621 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -165,7 +165,7 @@ export default abstract class FunctionBase extends NodeBase { for (const parameter of this.params) { if (parameter instanceof Identifier) { const parameterVariable = parameter.variable as ParameterVariable | null; - parameterVariable?.setKnownValue(UNKNOWN_EXPRESSION); + parameterVariable?.markReassigned(); } } } From 0f58a40d2ec2943fe0352c9a083e5468ffe5ae6e Mon Sep 17 00:00:00 2001 From: liuly Date: Fri, 19 Apr 2024 22:37:05 +0800 Subject: [PATCH 49/49] style: make coverage more happy --- src/ast/nodes/shared/FunctionBase.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ast/nodes/shared/FunctionBase.ts b/src/ast/nodes/shared/FunctionBase.ts index 83f19f72621..8bea01354eb 100644 --- a/src/ast/nodes/shared/FunctionBase.ts +++ b/src/ast/nodes/shared/FunctionBase.ts @@ -75,7 +75,7 @@ export default abstract class FunctionBase extends NodeBase { this.flags = setFlag(this.flags, Flag.generator, value); } - private knownParameterValues: ExpressionEntity[] = []; + private knownParameterValues: (ExpressionEntity | undefined)[] = []; private allArguments: InteractionCalledArguments[] = []; /** * update knownParameterValues when a call is made to this function @@ -87,8 +87,9 @@ export default abstract class FunctionBase extends NodeBase { // 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) { - break; + return; } const knownParameterValue = this.knownParameterValues[position]; @@ -140,6 +141,10 @@ export default abstract class FunctionBase extends NodeBase { * 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]); @@ -155,7 +160,8 @@ export default abstract class FunctionBase extends NodeBase { // Parameters without default values if (parameter instanceof Identifier) { const parameterVariable = parameter.variable as ParameterVariable | null; - const knownParameterValue = this.knownParameterValues[position] ?? UNKNOWN_EXPRESSION; + // Only the RestElement may be undefined + const knownParameterValue = this.knownParameterValues[position]!; parameterVariable?.setKnownValue(knownParameterValue); } } @@ -314,8 +320,7 @@ export default abstract class FunctionBase extends NodeBase { const isIIFE = this.parent.type === NodeType.CallExpression && (this.parent as CallExpression).callee === this; - const shoulOptimizeFunctionParameters = - (isIIFE || this.onlyFunctionCallUsed()) && this.allArguments.length > 0; + const shoulOptimizeFunctionParameters = isIIFE || this.onlyFunctionCallUsed(); if (shoulOptimizeFunctionParameters) { this.applyFunctionParameterOptimization(); } else if (this.functionParametersOptimized) {