Skip to content
Permalink
Browse files
Implement try-statement-deoptimization for feature detection, tree-sh…
…ake unused arguments (#2892)

* Use a Set for pure functions

* Include all statements directly in a try-statement-block

* Retain side-effect-free code in functions called from try-statement-blocks

* Deoptimize try blocks, tree-shake excessive parameters

* Limit deoptimization to direct call expressions and one level

* Also tree-shake namespace members

* Also tree-shake arguments corresponding to unused parameters

* Add option to deactivate the try statement deoptimization
  • Loading branch information
lukastaegert committed Jun 5, 2019
1 parent 6a8d5ef commit cf398aa1afdbb84b03ddcd2969035964fb6a2c8f
Show file tree
Hide file tree
Showing 125 changed files with 1,127 additions and 268 deletions.
@@ -825,6 +825,56 @@ const result = foo.bar;
const illegalAccess = foo.quux.tooDeep;
```

**treeshake.tryCatchDeoptimization**
Type: `boolean`<br>
CLI: `--treeshake.tryCatchDeoptimization`/`--no-treeshake.tryCatchDeoptimization`<br>
Default: `true`

By default, Rollup assumes that many builtin globals of the runtime behave according to the latest specs when tree-shaking and do not throw unexpected errors. In order to support e.g. feature detection workflows that rely on those errors being thrown, Rollup will by default deactivate tree-shaking inside try-statements. Furthermore, it will also deactivate tree-shaking inside functions that are called directly from a try-statement if Rollup can resolve the function. Set `treeshake.tryCatchDeoptimization` to `false` if you do not need this feature and want to have tree-shaking inside try-statements as well as inside functions called from those statements.

```js
function directlyCalled1() {
// as this function is directly called from a try-statement, it will be
// retained unmodified for tryCatchDeoptimization: true including staements
// that would usually be removed
Object.create(null);
notDirectlyCalled();
}
function directlyCalled2() {
Object.create(null);
notDirectlyCalled();
}
function notDirectlyCalled() {
// even if this function is retained, this will be removed as the function is
// never directly called from a try-statement
Object.create(null);
}
function test(callback) {
try {
// calls to otherwise side-effect-free global functions are retained
// inside try-statements for tryCatchDeoptimization: true
Object.create(null);
// directly resolvable calls will also be deoptimized
directlyCalled1();
// if a parameter is called, then all arguments passed to that function
// parameter will be deoptimized
callback();
// all calls will be retained but only calls of the form
// "identifier(someArguments)" will also deoptimize the target
(notDirectlyCalled && notDirectlyCalled)();
} catch {}
}
test(directlyCalled2);
```

### Experimental options

These options reflect new features that have not yet been fully finalized. Availability, behaviour and usage may therefore be subject to change between minor versions.
@@ -117,13 +117,16 @@ export default class Graph {
moduleSideEffects: (options.treeshake as TreeshakingOptions).moduleSideEffects,
propertyReadSideEffects:
(options.treeshake as TreeshakingOptions).propertyReadSideEffects !== false,
pureExternalModules: (options.treeshake as TreeshakingOptions).pureExternalModules
pureExternalModules: (options.treeshake as TreeshakingOptions).pureExternalModules,
tryCatchDeoptimization:
(options.treeshake as TreeshakingOptions).tryCatchDeoptimization !== false
}
: {
annotations: true,
moduleSideEffects: true,
propertyReadSideEffects: true,
pureExternalModules: false
pureExternalModules: false,
tryCatchDeoptimization: true
};
}

@@ -109,6 +109,7 @@ export interface AstContext {
traceExport: (name: string) => Variable;
traceVariable: (name: string) => Variable | null;
treeshake: boolean;
tryCatchDeoptimization: boolean;
usesTopLevelAwait: boolean;
warn: (warning: RollupWarning, pos: number) => void;
}
@@ -589,6 +590,8 @@ export default class Module {
traceExport: this.getVariableForExportName.bind(this),
traceVariable: this.traceVariable.bind(this),
treeshake: !!this.graph.treeshakingOptions,
tryCatchDeoptimization: (!this.graph.treeshakingOptions ||
this.graph.treeshakingOptions.tryCatchDeoptimization) as boolean,
usesTopLevelAwait: false,
warn: this.warn.bind(this)
};
@@ -631,7 +634,7 @@ export default class Module {
const otherModule = importDeclaration.module as Module | ExternalModule;

if (otherModule instanceof Module && importDeclaration.name === '*') {
return (otherModule).getOrCreateNamespace();
return otherModule.getOrCreateNamespace();
}

const declaration = otherModule.getVariableForExportName(importDeclaration.name);
@@ -10,7 +10,6 @@ import ThisVariable from './variables/ThisVariable';
export enum OptionTypes {
IGNORED_LABELS,
ACCESSED_NODES,
ARGUMENTS_VARIABLES,
ASSIGNED_NODES,
IGNORE_BREAK_STATEMENTS,
IGNORE_RETURN_AWAIT_YIELD,
@@ -78,10 +77,6 @@ export class ExecutionPathOptions {
);
}

getArgumentsVariables(): ExpressionEntity[] {
return (this.get(OptionTypes.ARGUMENTS_VARIABLES) || []) as ExpressionEntity[];
}

getHasEffectsWhenCalledOptions() {
return this.setIgnoreReturnAwaitYield()
.setIgnoreBreakStatements(false)
@@ -171,10 +166,6 @@ export class ExecutionPathOptions {
return this.setIn([OptionTypes.REPLACED_VARIABLE_INITS, variable], init);
}

setArgumentsVariables(variables: ExpressionEntity[]) {
return this.set(OptionTypes.ARGUMENTS_VARIABLES, variables);
}

setIgnoreBreakStatements(value = true) {
return this.set(OptionTypes.IGNORE_BREAK_STATEMENTS, value);
}
@@ -19,11 +19,13 @@ export default class ArrayPattern extends NodeBase implements PatternNode {
}

declare(kind: string, _init: ExpressionEntity) {
const variables = [];
for (const element of this.elements) {
if (element !== null) {
element.declare(kind, UNKNOWN_EXPRESSION);
variables.push(...element.declare(kind, UNKNOWN_EXPRESSION));
}
}
return variables;
}

deoptimizePath(path: ObjectPath) {
@@ -4,9 +4,12 @@ import ReturnValueScope from '../scopes/ReturnValueScope';
import Scope from '../scopes/Scope';
import { ObjectPath, UNKNOWN_EXPRESSION, UNKNOWN_KEY, UNKNOWN_PATH } from '../values';
import BlockStatement from './BlockStatement';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import RestElement from './RestElement';
import { ExpressionNode, GenericEsTreeNode, NodeBase } from './shared/Node';
import { PatternNode } from './shared/Pattern';
import SpreadElement from './SpreadElement';

export default class ArrowFunctionExpression extends NodeBase {
body!: BlockStatement | ExpressionNode;
@@ -57,10 +60,25 @@ export default class ArrowFunctionExpression extends NodeBase {
return this.body.hasEffects(options);
}

initialise() {
include(includeChildrenRecursively: boolean | 'variables') {
this.included = true;
this.body.include(includeChildrenRecursively);
for (const param of this.params) {
param.declare('parameter', UNKNOWN_EXPRESSION);
if (!(param instanceof Identifier)) {
param.include(includeChildrenRecursively);
}
}
}

includeCallArguments(args: (ExpressionNode | SpreadElement)[]): void {
this.scope.includeCallArguments(args);
}

initialise() {
this.scope.addParameterVariables(
this.params.map(param => param.declare('parameter', UNKNOWN_EXPRESSION)),
this.params[this.params.length - 1] instanceof RestElement
);
if (this.body instanceof BlockStatement) {
this.body.addImplicitReturnExpressionToScope();
} else {
@@ -25,7 +25,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode {
}

declare(kind: string, init: ExpressionEntity) {
this.left.declare(kind, init);
return this.left.declare(kind, init);
}

deoptimizePath(path: ObjectPath) {
@@ -4,7 +4,7 @@ import { ExecutionPathOptions } from '../ExecutionPathOptions';
import ArrowFunctionExpression from './ArrowFunctionExpression';
import * as NodeType from './NodeType';
import FunctionNode from './shared/FunctionNode';
import { ExpressionNode, Node, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, Node, NodeBase } from './shared/Node';

export default class AwaitExpression extends NodeBase {
argument!: ExpressionNode;
@@ -14,15 +14,15 @@ export default class AwaitExpression extends NodeBase {
return super.hasEffects(options) || !options.ignoreReturnAwaitYield();
}

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
if (!this.context.usesTopLevelAwait) {
include(includeChildrenRecursively: IncludeChildren) {
if (!this.included && !this.context.usesTopLevelAwait) {
let parent = this.parent;
do {
if (parent instanceof FunctionNode || parent instanceof ArrowFunctionExpression) return;
} while ((parent = (parent as Node).parent as Node));
this.context.usesTopLevelAwait = true;
}
super.include(includeChildrenRecursively);
}

render(code: MagicString, options: RenderOptions) {
@@ -6,7 +6,7 @@ import ChildScope from '../scopes/ChildScope';
import Scope from '../scopes/Scope';
import { UNKNOWN_EXPRESSION } from '../values';
import * as NodeType from './NodeType';
import { Node, StatementBase, StatementNode } from './shared/Node';
import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node';

export default class BlockStatement extends StatementBase {
body!: StatementNode[];
@@ -32,11 +32,11 @@ export default class BlockStatement extends StatementBase {
return false;
}

include(includeAllChildrenRecursively: boolean) {
include(includeChildrenRecursively: IncludeChildren) {
this.included = true;
for (const node of this.body) {
if (includeAllChildrenRecursively || node.shouldBeIncluded())
node.include(includeAllChildrenRecursively);
if (includeChildrenRecursively || node.shouldBeIncluded())
node.include(includeChildrenRecursively);
}
}

@@ -1,6 +1,10 @@
import MagicString from 'magic-string';
import { BLANK } from '../../utils/blank';
import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import {
findFirstOccurrenceOutsideComment,
NodeRenderOptions,
RenderOptions
} from '../../utils/renderHelpers';
import CallOptions from '../CallOptions';
import { DeoptimizableEntity } from '../DeoptimizableEntity';
import { ExecutionPathOptions } from '../ExecutionPathOptions';
@@ -19,7 +23,7 @@ import {
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { ExpressionEntity } from './shared/Expression';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, INCLUDE_VARIABLES, IncludeChildren, NodeBase } from './shared/Node';
import SpreadElement from './SpreadElement';

export default class CallExpression extends NodeBase implements DeoptimizableEntity {
@@ -196,8 +200,21 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt
);
}

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
include(includeChildrenRecursively: IncludeChildren) {
if (includeChildrenRecursively) {
super.include(includeChildrenRecursively);
if (
includeChildrenRecursively === INCLUDE_VARIABLES &&
this.callee instanceof Identifier &&
this.callee.variable
) {
this.callee.variable.includeInitRecursively();
}
} else {
this.included = true;
this.callee.include(false);
}
this.callee.includeCallArguments(this.arguments);
if (!(this.returnExpression as ExpressionEntity).included) {
(this.returnExpression as ExpressionEntity).include(false);
}
@@ -216,7 +233,30 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt
options: RenderOptions,
{ renderedParentType }: NodeRenderOptions = BLANK
) {
super.render(code, options);
this.callee.render(code, options);
if (this.arguments.length > 0) {
if (this.arguments[this.arguments.length - 1].included) {
for (const arg of this.arguments) {
arg.render(code, options);
}
} else {
let lastIncludedIndex = this.arguments.length - 2;
while (lastIncludedIndex >= 0 && !this.arguments[lastIncludedIndex].included) {
lastIncludedIndex--;
}
if (lastIncludedIndex >= 0) {
for (let index = 0; index <= lastIncludedIndex; index++) {
this.arguments[index].render(code, options);
}
code.remove(this.arguments[lastIncludedIndex].end, this.end - 1);
} else {
code.remove(
findFirstOccurrenceOutsideComment(code.original, '(', this.callee.end) + 1,
this.end - 1
);
}
}
}
if (
renderedParentType === NodeType.ExpressionStatement &&
this.callee.type === NodeType.FunctionExpression
@@ -20,7 +20,7 @@ import CallExpression from './CallExpression';
import * as NodeType from './NodeType';
import { ExpressionEntity } from './shared/Expression';
import { MultiExpression } from './shared/MultiExpression';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node';

export default class ConditionalExpression extends NodeBase implements DeoptimizableEntity {
alternate!: ExpressionNode;
@@ -133,14 +133,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz
return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, options);
}

include(includeAllChildrenRecursively: boolean) {
include(includeChildrenRecursively: IncludeChildren) {
this.included = true;
if (includeAllChildrenRecursively || this.usedBranch === null || this.test.shouldBeIncluded()) {
this.test.include(includeAllChildrenRecursively);
this.consequent.include(includeAllChildrenRecursively);
this.alternate.include(includeAllChildrenRecursively);
if (includeChildrenRecursively || this.usedBranch === null || this.test.shouldBeIncluded()) {
this.test.include(includeChildrenRecursively);
this.consequent.include(includeChildrenRecursively);
this.alternate.include(includeChildrenRecursively);
} else {
this.usedBranch.include(includeAllChildrenRecursively);
this.usedBranch.include(includeChildrenRecursively);
}
}

@@ -12,7 +12,7 @@ import ClassDeclaration from './ClassDeclaration';
import FunctionDeclaration from './FunctionDeclaration';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import { ExpressionNode, NodeBase } from './shared/Node';
import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node';

const WHITESPACE = /\s/;

@@ -43,9 +43,9 @@ export default class ExportDefaultDeclaration extends NodeBase {

private declarationName: string | undefined;

include(includeAllChildrenRecursively: boolean) {
super.include(includeAllChildrenRecursively);
if (includeAllChildrenRecursively) {
include(includeChildrenRecursively: IncludeChildren) {
super.include(includeChildrenRecursively);
if (includeChildrenRecursively) {
this.context.includeVariable(this.variable);
}
}

0 comments on commit cf398aa

Please sign in to comment.