Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle getters on functions and improve property deoptimization #4493

Merged
merged 8 commits into from May 13, 2022
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;