Skip to content

Commit

Permalink
Handle getters on functions and improve property deoptimization (#4493)
Browse files Browse the repository at this point in the history
* Do not make Object.defineProperty/ies a side effect by default

* Detect side effects for getters on functions

* Less deoptimization for non-accessor assignments

* Use ObjectEntity for arrow function properties

* Share code between functions and arrow functions

* Use new path key for defineProperty side effects

* Enable custom call-effect detection per global

* Improve coverage
  • Loading branch information
lukastaegert committed May 13, 2022
1 parent 8c6e0f3 commit f3a1fa3
Show file tree
Hide file tree
Showing 25 changed files with 429 additions and 257 deletions.
1 change: 1 addition & 0 deletions src/ast/Entity.ts
Expand Up @@ -12,5 +12,6 @@ export interface WritableEntity extends Entity {
* expression of this node is reassigned as well.
*/
deoptimizePath(path: ObjectPath): void;

hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean;
}
121 changes: 16 additions & 105 deletions src/ast/nodes/ArrowFunctionExpression.ts
@@ -1,104 +1,40 @@
import type { NormalizedTreeshakingOptions } from '../../rollup/types';
import { type CallOptions, NO_ARGS } from '../CallOptions';
import {
BROKEN_FLOW_NONE,
type HasEffectsContext,
type InclusionContext
} from '../ExecutionContext';
import { type CallOptions } from '../CallOptions';
import { type HasEffectsContext, InclusionContext } from '../ExecutionContext';
import ReturnValueScope from '../scopes/ReturnValueScope';
import type Scope from '../scopes/Scope';
import { type ObjectPath, UNKNOWN_PATH, UnknownKey } from '../utils/PathTracker';
import { type ObjectPath } from '../utils/PathTracker';
import BlockStatement from './BlockStatement';
import Identifier from './Identifier';
import * as NodeType from './NodeType';
import RestElement from './RestElement';
import type SpreadElement from './SpreadElement';
import { type ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression';
import {
type ExpressionNode,
type GenericEsTreeNode,
type IncludeChildren,
NodeBase
} from './shared/Node';
import FunctionBase from './shared/FunctionBase';
import { type ExpressionNode, IncludeChildren } from './shared/Node';
import { ObjectEntity } from './shared/ObjectEntity';
import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype';
import type { PatternNode } from './shared/Pattern';

export default class ArrowFunctionExpression extends NodeBase {
export default class ArrowFunctionExpression extends FunctionBase {
declare async: boolean;
declare body: BlockStatement | ExpressionNode;
declare params: readonly PatternNode[];
declare preventChildBlockScope: true;
declare scope: ReturnValueScope;
declare type: NodeType.tArrowFunctionExpression;
private deoptimizedReturn = false;
protected objectEntity: ObjectEntity | null = null;

createScope(parentScope: Scope): void {
this.scope = new ReturnValueScope(parentScope, this.context);
}

deoptimizePath(path: ObjectPath): void {
// A reassignment of UNKNOWN_PATH is considered equivalent to having lost track
// which means the return expression needs to be reassigned
if (path.length === 1 && path[0] === UnknownKey) {
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
}
}

// Arrow functions do not mutate their context
deoptimizeThisOnEventAtPath(): void {}

getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity {
if (path.length !== 0) {
return UNKNOWN_EXPRESSION;
}
if (this.async) {
if (!this.deoptimizedReturn) {
this.deoptimizedReturn = true;
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
this.context.requestTreeshakingPass();
}
return UNKNOWN_EXPRESSION;
}
return this.scope.getReturnExpression();
}

hasEffects(): boolean {
return false;
}

hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean {
return path.length > 1;
}

hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean {
return path.length > 1;
}

hasEffectsWhenCalledAtPath(
path: ObjectPath,
_callOptions: CallOptions,
callOptions: CallOptions,
context: HasEffectsContext
): boolean {
if (path.length > 0) return true;
if (this.async) {
const { propertyReadSideEffects } = this.context.options
.treeshake as NormalizedTreeshakingOptions;
const returnExpression = this.scope.getReturnExpression();
if (
returnExpression.hasEffectsWhenCalledAtPath(
['then'],
{ args: NO_ARGS, thisParam: null, withNew: false },
context
) ||
(propertyReadSideEffects &&
(propertyReadSideEffects === 'always' ||
returnExpression.hasEffectsWhenAccessedAtPath(['then'], context)))
) {
return true;
}
}
for (const param of this.params) {
if (param.hasEffects(context)) return true;
}
if (super.hasEffectsWhenCalledAtPath(path, callOptions, context)) return true;
const { ignore, brokenFlow } = context;
context.ignore = {
breaks: false,
Expand All @@ -113,43 +49,18 @@ export default class ArrowFunctionExpression extends NodeBase {
}

include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
this.included = true;
super.include(context, includeChildrenRecursively);
for (const param of this.params) {
if (!(param instanceof Identifier)) {
param.include(context, includeChildrenRecursively);
}
}
const { brokenFlow } = context;
context.brokenFlow = BROKEN_FLOW_NONE;
this.body.include(context, includeChildrenRecursively);
context.brokenFlow = brokenFlow;
}

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

initialise(): void {
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 {
this.scope.addReturnExpression(this.body);
}
}

parseNode(esTreeNode: GenericEsTreeNode): void {
if (esTreeNode.body.type === NodeType.BlockStatement) {
this.body = new BlockStatement(esTreeNode.body, this, this.scope.hoistedBodyVarScope);
protected getObjectEntity(): ObjectEntity {
if (this.objectEntity !== null) {
return this.objectEntity;
}
super.parseNode(esTreeNode);
return (this.objectEntity = new ObjectEntity([], OBJECT_PROTOTYPE));
}
}

ArrowFunctionExpression.prototype.preventChildBlockScope = true;
9 changes: 7 additions & 2 deletions src/ast/nodes/MemberExpression.ts
Expand Up @@ -14,7 +14,8 @@ import {
type PathTracker,
SHARED_RECURSION_TRACKER,
UNKNOWN_PATH,
UnknownKey
UnknownKey,
UnknownNonAccessorKey
} from '../utils/PathTracker';
import ExternalVariable from '../variables/ExternalVariable';
import type NamespaceVariable from '../variables/NamespaceVariable';
Expand Down Expand Up @@ -128,7 +129,11 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE
this.variable.deoptimizePath(path);
} else if (!this.replacement) {
if (path.length < MAX_PATH_DEPTH) {
this.object.deoptimizePath([this.getPropertyKey(), ...path]);
const propertyKey = this.getPropertyKey();
this.object.deoptimizePath([
propertyKey === UnknownKey ? UnknownNonAccessorKey : propertyKey,
...path
]);
}
}
}
Expand Down
169 changes: 169 additions & 0 deletions src/ast/nodes/shared/FunctionBase.ts
@@ -0,0 +1,169 @@
import type { NormalizedTreeshakingOptions } from '../../../rollup/types';
import { type CallOptions, NO_ARGS } from '../../CallOptions';
import { DeoptimizableEntity } from '../../DeoptimizableEntity';
import {
BROKEN_FLOW_NONE,
type HasEffectsContext,
type InclusionContext
} from '../../ExecutionContext';
import { NodeEvent } from '../../NodeEvents';
import ReturnValueScope from '../../scopes/ReturnValueScope';
import { type ObjectPath, PathTracker, UNKNOWN_PATH, UnknownKey } from '../../utils/PathTracker';
import BlockStatement from '../BlockStatement';
import * as NodeType from '../NodeType';
import RestElement from '../RestElement';
import type SpreadElement from '../SpreadElement';
import { type ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression';
import {
type ExpressionNode,
type GenericEsTreeNode,
type IncludeChildren,
NodeBase
} from './Node';
import { ObjectEntity } from './ObjectEntity';
import type { PatternNode } from './Pattern';

export default abstract class FunctionBase extends NodeBase {
declare async: boolean;
declare body: BlockStatement | ExpressionNode;
declare params: readonly PatternNode[];
declare preventChildBlockScope: true;
declare scope: ReturnValueScope;
protected objectEntity: ObjectEntity | null = null;
private deoptimizedReturn = false;

deoptimizePath(path: ObjectPath): void {
this.getObjectEntity().deoptimizePath(path);
if (path.length === 1 && path[0] === UnknownKey) {
// A reassignment of UNKNOWN_PATH is considered equivalent to having lost track
// which means the return expression needs to be reassigned
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
}
}

deoptimizeThisOnEventAtPath(
event: NodeEvent,
path: ObjectPath,
thisParameter: ExpressionEntity,
recursionTracker: PathTracker
): void {
if (path.length > 0) {
this.getObjectEntity().deoptimizeThisOnEventAtPath(
event,
path,
thisParameter,
recursionTracker
);
}
}

getLiteralValueAtPath(
path: ObjectPath,
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin);
}

getReturnExpressionWhenCalledAtPath(
path: ObjectPath,
callOptions: CallOptions,
recursionTracker: PathTracker,
origin: DeoptimizableEntity
): ExpressionEntity {
if (path.length > 0) {
return this.getObjectEntity().getReturnExpressionWhenCalledAtPath(
path,
callOptions,
recursionTracker,
origin
);
}
if (this.async) {
if (!this.deoptimizedReturn) {
this.deoptimizedReturn = true;
this.scope.getReturnExpression().deoptimizePath(UNKNOWN_PATH);
this.context.requestTreeshakingPass();
}
return UNKNOWN_EXPRESSION;
}
return this.scope.getReturnExpression();
}

hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean {
return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context);
}

hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean {
return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context);
}

hasEffectsWhenCalledAtPath(
path: ObjectPath,
callOptions: CallOptions,
context: HasEffectsContext
): boolean {
if (path.length > 0) {
return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context);
}
if (this.async) {
const { propertyReadSideEffects } = this.context.options
.treeshake as NormalizedTreeshakingOptions;
const returnExpression = this.scope.getReturnExpression();
if (
returnExpression.hasEffectsWhenCalledAtPath(
['then'],
{ args: NO_ARGS, thisParam: null, withNew: false },
context
) ||
(propertyReadSideEffects &&
(propertyReadSideEffects === 'always' ||
returnExpression.hasEffectsWhenAccessedAtPath(['then'], context)))
) {
return true;
}
}
for (const param of this.params) {
if (param.hasEffects(context)) return true;
}
return false;
}

include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void {
this.included = true;
const { brokenFlow } = context;
context.brokenFlow = BROKEN_FLOW_NONE;
this.body.include(context, includeChildrenRecursively);
context.brokenFlow = brokenFlow;
}

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

initialise(): void {
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 {
this.scope.addReturnExpression(this.body);
}
}

parseNode(esTreeNode: GenericEsTreeNode): void {
if (esTreeNode.body.type === NodeType.BlockStatement) {
this.body = new BlockStatement(esTreeNode.body, this, this.scope.hoistedBodyVarScope);
}
super.parseNode(esTreeNode);
}

protected abstract getObjectEntity(): ObjectEntity;
}

FunctionBase.prototype.preventChildBlockScope = true;

0 comments on commit f3a1fa3

Please sign in to comment.