Skip to content

Commit

Permalink
Treeshake instanceof
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Jun 8, 2022
1 parent 0ab16cc commit 9bf5bdb
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 11 deletions.
83 changes: 77 additions & 6 deletions src/ast/nodes/BinaryExpression.ts
Expand Up @@ -2,22 +2,52 @@ import type MagicString from 'magic-string';
import { BLANK } from '../../utils/blank';
import type { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers';
import type { DeoptimizableEntity } from '../DeoptimizableEntity';
import type { HasEffectsContext } from '../ExecutionContext';
import { INTERACTION_ACCESSED, NodeInteraction } from '../NodeInteractions';
import type { HasEffectsContext, InclusionContext } from '../ExecutionContext';
import { createHasEffectsContext } from '../ExecutionContext';
import {
INTERACTION_ACCESSED,
NODE_INTERACTION_UNKNOWN_ASSIGNMENT,
NodeInteraction
} from '../NodeInteractions';
import {
EMPTY_PATH,
type ObjectPath,
type PathTracker,
SHARED_RECURSION_TRACKER
SHARED_RECURSION_TRACKER,
TEST_INCLUSION_PATH
} from '../utils/PathTracker';
import ExpressionStatement from './ExpressionStatement';
import type { LiteralValue } from './Literal';
import type * as NodeType from './NodeType';
import { type LiteralValueOrUnknown, UnknownValue } from './shared/Expression';
import { type ExpressionNode, NodeBase } from './shared/Node';
import { type ExpressionNode, IncludeChildren, NodeBase } from './shared/Node';

type Operator =
| '!='
| '!=='
| '%'
| '&'
| '*'
| '**'
| '+'
| '-'
| '/'
| '<'
| '<<'
| '<='
| '=='
| '==='
| '>'
| '>='
| '>>'
| '>>>'
| '^'
| '|'
| 'in'
| 'instanceof';

const binaryOperators: {
[operator: string]: (left: LiteralValue, right: LiteralValue) => LiteralValueOrUnknown;
[operator in Operator]?: (left: LiteralValue, right: LiteralValue) => LiteralValueOrUnknown;
} = {
'!=': (left, right) => left != right,
'!==': (left, right) => left !== right,
Expand Down Expand Up @@ -52,6 +82,7 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
declare operator: keyof typeof binaryOperators;
declare right: ExpressionNode;
declare type: NodeType.tBinaryExpression;
private expressionsDependingOnNegativeInstanceof = new Set<DeoptimizableEntity>();

deoptimizeCache(): void {}

Expand All @@ -61,6 +92,19 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
origin: DeoptimizableEntity
): LiteralValueOrUnknown {
if (path.length > 0) return UnknownValue;
if (this.operator === 'instanceof') {
if (
this.right.hasEffectsOnInteractionAtPath(
TEST_INCLUSION_PATH,
NODE_INTERACTION_UNKNOWN_ASSIGNMENT,
createHasEffectsContext()
)
) {
return UnknownValue;
}
this.expressionsDependingOnNegativeInstanceof.add(origin);
return false;
}
const leftValue = this.left.getLiteralValueAtPath(EMPTY_PATH, recursionTracker, origin);
if (typeof leftValue === 'symbol') return UnknownValue;

Expand All @@ -79,15 +123,25 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
this.operator === '+' &&
this.parent instanceof ExpressionStatement &&
this.left.getLiteralValueAtPath(EMPTY_PATH, SHARED_RECURSION_TRACKER, this) === ''
)
) {
return true;
}
this.deoptimizeInstanceof(context);
return super.hasEffects(context);
}

hasEffectsOnInteractionAtPath(path: ObjectPath, { type }: NodeInteraction): boolean {
return type !== INTERACTION_ACCESSED || path.length > 1;
}

// TODO Lukas if the operator is instanceof, only include if necessary
include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) {
this.deoptimizeInstanceof();
if (!this.expressionsDependingOnNegativeInstanceof.size) {
super.include(context, includeChildrenRecursively);
}
}

render(
code: MagicString,
options: RenderOptions,
Expand All @@ -96,4 +150,21 @@ export default class BinaryExpression extends NodeBase implements DeoptimizableE
this.left.render(code, options, { renderedSurroundingElement });
this.right.render(code, options);
}

private deoptimizeInstanceof(context?: HasEffectsContext) {
if (
this.operator === 'instanceof' &&
this.expressionsDependingOnNegativeInstanceof.size &&
this.right.hasEffectsOnInteractionAtPath(
TEST_INCLUSION_PATH,
NODE_INTERACTION_UNKNOWN_ASSIGNMENT,
context || createHasEffectsContext()
)
) {
for (const expression of this.expressionsDependingOnNegativeInstanceof) {
expression.deoptimizeCache();
}
this.expressionsDependingOnNegativeInstanceof.clear();
}
}
}
2 changes: 2 additions & 0 deletions src/ast/nodes/shared/ObjectEntity.ts
Expand Up @@ -11,6 +11,7 @@ import {
ObjectPath,
ObjectPathKey,
PathTracker,
TestInclusionKey,
UNKNOWN_INTEGER_PATH,
UNKNOWN_PATH,
UnknownInteger,
Expand Down Expand Up @@ -281,6 +282,7 @@ export class ObjectEntity extends ExpressionEntity {
context: HasEffectsContext
): boolean {
const [key, ...subPath] = path;
if (key === TestInclusionKey) return false;
if (subPath.length || interaction.type === INTERACTION_CALLED) {
const expressionAtPath = this.getMemberExpression(key);
if (expressionAtPath) {
Expand Down
12 changes: 11 additions & 1 deletion src/ast/utils/PathTracker.ts
Expand Up @@ -4,15 +4,23 @@ import type { Entity } from '../Entity';
export const UnknownKey = Symbol('Unknown Key');
export const UnknownNonAccessorKey = Symbol('Unknown Non-Accessor Key');
export const UnknownInteger = Symbol('Unknown Integer');
/**
* A special key that does not actually reference a property but can be used to
* test if a variable is included via
* .hasEffectsOnInteractionAtPath([TestInclusionKey], {type: INTERACTION_ASSIGNED,...}, ...)
*/
export const TestInclusionKey = Symbol('Test Inclusion Key');
export type ObjectPathKey =
| string
| typeof UnknownKey
| typeof UnknownNonAccessorKey
| typeof UnknownInteger;
| typeof UnknownInteger
| typeof TestInclusionKey;

export type ObjectPath = ObjectPathKey[];
export const EMPTY_PATH: ObjectPath = [];
export const UNKNOWN_PATH: ObjectPath = [UnknownKey];
export const TEST_INCLUSION_PATH: ObjectPath = [TestInclusionKey];
// For deoptimizations, this means we are modifying an unknown property but did
// not lose track of the object or are creating a setter/getter;
// For assignment effects it means we do not check for setter/getter effects
Expand All @@ -25,6 +33,7 @@ const EntitiesKey = Symbol('Entities');
interface EntityPaths {
[pathSegment: string]: EntityPaths;
[EntitiesKey]: Set<Entity>;
[TestInclusionKey]?: EntityPaths;
[UnknownInteger]?: EntityPaths;
[UnknownKey]?: EntityPaths;
[UnknownNonAccessorKey]?: EntityPaths;
Expand Down Expand Up @@ -72,6 +81,7 @@ export const SHARED_RECURSION_TRACKER = new PathTracker();
interface DiscriminatedEntityPaths {
[pathSegment: string]: DiscriminatedEntityPaths;
[EntitiesKey]: Map<unknown, Set<Entity>>;
[TestInclusionKey]?: DiscriminatedEntityPaths;
[UnknownInteger]?: DiscriminatedEntityPaths;
[UnknownKey]?: DiscriminatedEntityPaths;
[UnknownNonAccessorKey]?: DiscriminatedEntityPaths;
Expand Down
3 changes: 3 additions & 0 deletions test/form/samples/instanceof/_config.js
@@ -0,0 +1,3 @@
module.exports = {
description: 'removes instanceof checks if the right side is not included'
};
3 changes: 3 additions & 0 deletions test/form/samples/instanceof/_expected.js
@@ -0,0 +1,3 @@
console.log('retained');

console.log('retained');
17 changes: 17 additions & 0 deletions test/form/samples/instanceof/main.js
@@ -0,0 +1,17 @@
class Unused1 {}
class Used1 {}
const Intermediate = Used1;

if (new Intermediate() instanceof Unused1) console.log('removed');
else console.log('retained');

class Unused2 {}
class WithEffect {
constructor() {
console.log('effect');
}
}

if (new WithEffect() instanceof Unused2)
console.log('does not matter, but effect should be retained');
else console.log('retained');
4 changes: 0 additions & 4 deletions test/form/samples/invalid-binary-expressions/_expected.js
Expand Up @@ -37,7 +37,3 @@ if (null instanceof true) {
if (null instanceof 'y') {
console.log('retained');
}

if (null instanceof {}) {
console.log('retained');
}

0 comments on commit 9bf5bdb

Please sign in to comment.