From 7b8304e46effd4a45df76193303a2085cf023136 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 25 Mar 2021 15:35:44 +0100 Subject: [PATCH 01/50] Move logic from ClassBody into ClassNode So that it sits in one place and is easier to extend. --- src/ast/nodes/ClassBody.ts | 27 --------------------------- src/ast/nodes/shared/ClassNode.ts | 21 +++++++++++++++------ 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/ast/nodes/ClassBody.ts b/src/ast/nodes/ClassBody.ts index 0dc6efa32a5..cd0d21c907e 100644 --- a/src/ast/nodes/ClassBody.ts +++ b/src/ast/nodes/ClassBody.ts @@ -1,8 +1,5 @@ -import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; import ClassBodyScope from '../scopes/ClassBodyScope'; import Scope from '../scopes/Scope'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import MethodDefinition from './MethodDefinition'; import * as NodeType from './NodeType'; import PropertyDefinition from './PropertyDefinition'; @@ -12,31 +9,7 @@ export default class ClassBody extends NodeBase { body!: (MethodDefinition | PropertyDefinition)[]; type!: NodeType.tClassBody; - private classConstructor!: MethodDefinition | null; - createScope(parentScope: Scope) { this.scope = new ClassBodyScope(parentScope); } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (path.length > 0) return true; - return ( - this.classConstructor !== null && - this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); - } - - initialise() { - for (const method of this.body) { - if (method instanceof MethodDefinition && method.kind === 'constructor') { - this.classConstructor = method; - return; - } - } - this.classConstructor = null; - } } diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 4323d8c789f..c6fe89cc24d 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -2,15 +2,17 @@ import { CallOptions } from '../../CallOptions'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { ObjectPath } from '../../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath } from '../../utils/PathTracker'; import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; +import MethodDefinition from '../MethodDefinition'; import { ExpressionNode, NodeBase } from './Node'; export default class ClassNode extends NodeBase { body!: ClassBody; id!: Identifier | null; superClass!: ExpressionNode | null; + private classConstructor!: MethodDefinition | null; createScope(parentScope: Scope) { this.scope = new ChildScope(parentScope); @@ -31,17 +33,24 @@ export default class ClassNode extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ) { - if (!callOptions.withNew) return true; - return ( - this.body.hasEffectsWhenCalledAtPath(path, callOptions, context) || + return !callOptions.withNew || + path.length > 0 || + (this.classConstructor !== null && + this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context)) || (this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) - ); + this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)); } initialise() { if (this.id !== null) { this.id.declare('class', this); } + for (const method of this.body.body) { + if (method instanceof MethodDefinition && method.kind === 'constructor') { + this.classConstructor = method; + return; + } + } + this.classConstructor = null; } } From d524abd6b8c08a21f4fd91a474575ce406878b56 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 25 Mar 2021 19:38:37 +0100 Subject: [PATCH 02/50] Track static class fields and improve handling of class getters/setters This aims to improve tree-shaking of code that uses static class properties (#3989) and to improve detection of side effects through class getters/setters (#4016). The first part works by keeping a map of positively known static properties (methods and simple getters) in `ClassNode.staticPropertyMap`, along with a flag (`ClassNode.deoptimizedStatic`) that indicates that something happened that removed our confidence that we know anything about the class object. Access and calls to these known static properties are handled by routing the calls to `getLiteralValueAtPath`, `getReturnExpressionWhenCalledAtPath`, and `hasEffectsWhenCalledAtPath` to the known values in the properties. In contrast to `ObjectExpression`, this class does not try to keep track of multiple expressions associated with a property, since that doesn't come up a lot on classes. The handling of side effect detection through getters and setters is done by, _if_ the entire class object (or its prototype in case of access to the prototype) hasn't been deoptimized, scanning through the directly defined getters and setters to see if one exists (calling through to superclasses as appropriate). I believe that this is solid because any code that would be able to change the set of getters and setters on a class would cause the entire object to be deoptimized. --- src/ast/nodes/shared/ClassNode.ts | 212 ++++++++++++++++-- .../literals-from-class-statics/_config.js | 3 + .../literals-from-class-statics/_expected.js | 13 ++ .../literals-from-class-statics/main.js | 21 ++ .../_config.js | 3 + .../_expected.js | 35 +++ .../main.js | 70 ++++++ .../side-effects-static-methods/_config.js | 3 + .../side-effects-static-methods/_expected.js | 38 ++++ .../side-effects-static-methods/main.js | 43 ++++ 10 files changed, 428 insertions(+), 13 deletions(-) create mode 100644 test/form/samples/literals-from-class-statics/_config.js create mode 100644 test/form/samples/literals-from-class-statics/_expected.js create mode 100644 test/form/samples/literals-from-class-statics/main.js create mode 100644 test/form/samples/side-effects-class-getters-setters/_config.js create mode 100644 test/form/samples/side-effects-class-getters-setters/_expected.js create mode 100644 test/form/samples/side-effects-class-getters-setters/main.js create mode 100644 test/form/samples/side-effects-static-methods/_config.js create mode 100644 test/form/samples/side-effects-static-methods/_expected.js create mode 100644 test/form/samples/side-effects-static-methods/main.js diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index c6fe89cc24d..3a11d5f77d4 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,11 +1,15 @@ +import { getOrCreate } from '../../../utils/getOrCreate'; import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { EMPTY_PATH, ObjectPath } from '../../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import MethodDefinition from '../MethodDefinition'; +import { ExpressionEntity } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; export default class ClassNode extends NodeBase { @@ -13,19 +17,137 @@ export default class ClassNode extends NodeBase { id!: Identifier | null; superClass!: ExpressionNode | null; private classConstructor!: MethodDefinition | null; + private deoptimizedPrototype = false; + private deoptimizedStatic = false; + // Collect deoptimization information if we can resolve a property access, by property name + private expressionsToBeDeoptimized = new Map(); + // Known, simple, non-deoptimized static properties are kept in here. They are removed when deoptimized. + private staticPropertyMap: {[name: string]: ExpressionNode} | null = null; + + bind() { + super.bind(); + } createScope(parentScope: Scope) { this.scope = new ChildScope(parentScope); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + deoptimizeAllStatics() { + for (const name in this.staticPropertyMap) { + this.deoptimizeStatic(name); + } + this.deoptimizedStatic = this.deoptimizedPrototype = true; + } + + deoptimizeCache() { + this.deoptimizeAllStatics(); + } + + deoptimizePath(path: ObjectPath) { + const propertyMap = this.getStaticPropertyMap(); + const key = path[0]; + const definition = typeof key === 'string' && propertyMap[key]; + if (path.length === 1) { + if (definition) { + this.deoptimizeStatic(key as string); + } else if (typeof key !== 'string') { + this.deoptimizeAllStatics(); + } + } else if (key === 'prototype' && typeof path[1] !== 'string') { + this.deoptimizedPrototype = true; + } else if (path.length > 1 && definition) { + definition.deoptimizePath(path.slice(1)); + } + } + + deoptimizeStatic(name: string) { + delete this.staticPropertyMap![name]; + const deoptEntities = this.expressionsToBeDeoptimized.get(name); + if (deoptEntities) { + for (const entity of deoptEntities) { + entity.deoptimizeCache(); + } + } + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + const key = path[0]; + const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; + if (path.length === 0 || !definition || + (key === 'prototype' ? this.deoptimizedPrototype : this.deoptimizedStatic)) { + return UnknownValue; + } + + getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); + return definition.getLiteralValueAtPath( + path.slice(1), + recursionTracker, + origin + ); } - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - if (path.length <= 1) return false; - return path.length > 2 || path[0] !== 'prototype'; + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + const key = path[0]; + const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; + + if (path.length === 0 || !definition || + (key === 'prototype' ? this.deoptimizedPrototype : this.deoptimizedStatic)) { + return UNKNOWN_EXPRESSION; + } + + getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); + return definition.getReturnExpressionWhenCalledAtPath( + path.slice(1), + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + if (path.length === 0) return false; + if (this.deoptimizedStatic) return true; + if (this.superClass && this.superClass.hasEffectsWhenAccessedAtPath(path, context)) return true; + const key = path[0]; + if (key === 'prototype') { + if (path.length === 1) return false; + if (this.deoptimizedPrototype) return true; + const key2 = path[1]; + if (path.length === 2 && typeof key2 === 'string') { + return mayHaveGetterSetterEffect(this.body, 'get', false, key2, context); + } + return true; + } else if (typeof key === 'string' && path.length === 1) { + return mayHaveGetterSetterEffect(this.body, 'get', true, key, context); + } else { + return true; + } + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + if (this.deoptimizedStatic) return true; + if (this.superClass && this.superClass.hasEffectsWhenAssignedAtPath(path, context)) return true; + const key = path[0]; + if (key === 'prototype') { + if (path.length === 1) return false; + if (this.deoptimizedPrototype) return true; + const key2 = path[1]; + if (path.length === 2 && typeof key2 === 'string') { + return mayHaveGetterSetterEffect(this.body, 'set', false, key2, context); + } + return true; + } else if (typeof key === 'string' && path.length === 1) { + return mayHaveGetterSetterEffect(this.body, 'set', true, key, context); + } else { + return true; + } } hasEffectsWhenCalledAtPath( @@ -33,12 +155,19 @@ export default class ClassNode extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ) { - return !callOptions.withNew || - path.length > 0 || - (this.classConstructor !== null && - this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context)) || - (this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)); + if (callOptions.withNew) { + return path.length > 0 || + (this.classConstructor !== null && + this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context)) || + (this.superClass !== null && + this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)); + } else { + if (path.length !== 1 || this.deoptimizedStatic) return true; + const key = path[0]; + const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; + if (!definition) return true; + return definition.hasEffectsWhenCalledAtPath([], callOptions, context); + } } initialise() { @@ -53,4 +182,61 @@ export default class ClassNode extends NodeBase { } this.classConstructor = null; } + + mayModifyThisWhenCalledAtPath( + path: ObjectPath + ) { + const key = path[0]; + const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; + if (!definition || this.deoptimizedStatic) return true; + return definition.mayModifyThisWhenCalledAtPath(path.slice(1)); + } + + private getStaticPropertyMap(): {[name: string]: ExpressionNode} { + if (this.staticPropertyMap) return this.staticPropertyMap; + + const propertyMap = this.staticPropertyMap = Object.create(null); + const seen: {[name: string]: boolean} = Object.create(null); + for (const definition of this.body.body) { + if (!definition.static) continue; + // If there are non-identifier-named statics, give up. + if (definition.computed || !(definition.key instanceof Identifier)) { + return this.staticPropertyMap = Object.create(null); + } + const key = definition.key.name; + // Not handling duplicate definitions. + if (seen[key]) { + delete propertyMap[key]; + continue; + } + seen[key] = true; + if (definition instanceof MethodDefinition) { + if (definition.kind === "method") { + propertyMap[key] = definition.value; + } + } else if (definition.value) { + propertyMap[key] = definition.value; + } + } + return this.staticPropertyMap = propertyMap; + } +} + +function mayHaveGetterSetterEffect( + body: ClassBody, + kind: 'get' | 'set', isStatic: boolean, name: string, + context: HasEffectsContext +) { + for (const definition of body.body) { + if (definition instanceof MethodDefinition && definition.static === isStatic && definition.kind === kind) { + if (definition.computed || !(definition.key instanceof Identifier)) { + return true; + } + if (definition.key.name === name && + definition.value.hasEffectsWhenCalledAtPath([], {args: [], withNew: false}, context)) { + return true; + } + } + } + return false; } diff --git a/test/form/samples/literals-from-class-statics/_config.js b/test/form/samples/literals-from-class-statics/_config.js new file mode 100644 index 00000000000..0847b183aac --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks literal values in class static fields' +}; diff --git a/test/form/samples/literals-from-class-statics/_expected.js b/test/form/samples/literals-from-class-statics/_expected.js new file mode 100644 index 00000000000..849a60add18 --- /dev/null +++ b/test/form/samples/literals-from-class-statics/_expected.js @@ -0,0 +1,13 @@ +log("t"); +log("x"); + +class Undef { + static y; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/literals-from-class-statics/main.js b/test/form/samples/literals-from-class-statics/main.js new file mode 100644 index 00000000000..13c231108ff --- /dev/null +++ b/test/form/samples/literals-from-class-statics/main.js @@ -0,0 +1,21 @@ +class Static { + static t() { return true; } + static f() { return false; } + static x = 10; +} + +if (Static.t()) log("t"); +if (Static.f()) log("f"); +if (!Static.t()) log("!t"); +if (Static.x) log("x"); + +class Undef { + static y; +} +if (Undef.y) log("y"); + +class Deopt { + static z = false; +} +unknown(Deopt); +if (Deopt.z) log("z"); diff --git a/test/form/samples/side-effects-class-getters-setters/_config.js b/test/form/samples/side-effects-class-getters-setters/_config.js new file mode 100644 index 00000000000..201e3e082f9 --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'treat getters and setters on classes as function calls' +}; diff --git a/test/form/samples/side-effects-class-getters-setters/_expected.js b/test/form/samples/side-effects-class-getters-setters/_expected.js new file mode 100644 index 00000000000..a6beaf62970 --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/_expected.js @@ -0,0 +1,35 @@ +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; + +class DeoptProto {} +unknown(DeoptProto.prototype); +DeoptProto.prototype.a; + +class DeoptComputed { + static get a() {} + static get [unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-class-getters-setters/main.js b/test/form/samples/side-effects-class-getters-setters/main.js new file mode 100644 index 00000000000..b5c78edf787 --- /dev/null +++ b/test/form/samples/side-effects-class-getters-setters/main.js @@ -0,0 +1,70 @@ +class Removed { + get a() { log(); } + set a(v) { log(); } + static get a() { log(); } + static set a(v) { log(); } +} + +class RemovedNoEffect { + get a() {} + set a(v) {} + static get a() {} + static set a(v) {} +} +RemovedNoEffect.prototype.a; +RemovedNoEffect.prototype.a = 1; +RemovedNoEffect.a; +RemovedNoEffect.a = 1; + +class RetainedByGetter { + get a() { log(); } +} +RetainedByGetter.prototype.a; + +class RetainedBySetter { + set a(v) { log(); } +} +RetainedBySetter.prototype.a = 10; + +class RetainedByStaticGetter { + static get a() { log(); } +} +RetainedByStaticGetter.a; + +class RetainedByStaticSetter { + static set a(v) { log(); } +} +RetainedByStaticSetter.a = 10; + +class RemovedSetters { + set a(v) { log(); } + static set a(v) { log(); } +} +RemovedSetters.prototype.a; +RemovedSetters.a; + +class RemovedWrongProp { + get a() { log(); } + static get a() { log(); } +} +RemovedWrongProp.prototype.b +RemovedWrongProp.b + +class RetainedSuper { + static get a() { log(); } +} +class RetainedSub extends RetainedSuper {} +RetainedSub.a; + +class RemovedSub extends RetainedSuper {} +RemovedSub.b; + +class DeoptProto {} +unknown(DeoptProto.prototype); +DeoptProto.prototype.a; + +class DeoptComputed { + static get a() {} + static get [unknown]() { log(); } +} +DeoptComputed.a; diff --git a/test/form/samples/side-effects-static-methods/_config.js b/test/form/samples/side-effects-static-methods/_config.js new file mode 100644 index 00000000000..129d0e1d6de --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'allow calls to pure static methods to be tree-shaken' +}; diff --git a/test/form/samples/side-effects-static-methods/_expected.js b/test/form/samples/side-effects-static-methods/_expected.js new file mode 100644 index 00000000000..3b12efac3e3 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/_expected.js @@ -0,0 +1,38 @@ +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; diff --git a/test/form/samples/side-effects-static-methods/main.js b/test/form/samples/side-effects-static-methods/main.js new file mode 100644 index 00000000000..f2d3e1036b4 --- /dev/null +++ b/test/form/samples/side-effects-static-methods/main.js @@ -0,0 +1,43 @@ +class NoEffect { + static a() {} +} +NoEffect.a(); + +class Effect { + static a() { log(); } +} +Effect.a(); + +class DeoptComputed { + static a() {} + static [foo]() { log(); } +} +DeoptComputed.a(); + +class DeoptGetter { + static a() {} + static get a() {} +} +DeoptGetter.a(); + +class DeoptAssign { + static a() {} +} +DeoptAssign.a = log; +DeoptAssign.a(); + +class DeoptFully { + static a() {} +} +unknown(DeoptFully); +DeoptFully.a(); + +class DeepAssign { + static a = {} + a = {} +} +DeepAssign.a.b = 1; +DeepAssign.prototype.a.b = 1; + +class DynamicAssign {} +DynamicAssign[foo()] = 1; From 185097db1327bc57973ded8f4a4340ead7a2641d Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sat, 27 Mar 2021 11:38:03 +0100 Subject: [PATCH 03/50] Remove ClassNode.deoptimizeCache --- src/ast/nodes/shared/ClassNode.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 3a11d5f77d4..2922e44fe6b 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -39,10 +39,6 @@ export default class ClassNode extends NodeBase { this.deoptimizedStatic = this.deoptimizedPrototype = true; } - deoptimizeCache() { - this.deoptimizeAllStatics(); - } - deoptimizePath(path: ObjectPath) { const propertyMap = this.getStaticPropertyMap(); const key = path[0]; From 0960a3384606e5e353884768a4af5dca66065005 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sat, 27 Mar 2021 11:58:05 +0100 Subject: [PATCH 04/50] Keep a table for class property effects --- src/ast/nodes/shared/ClassNode.ts | 54 ++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 2922e44fe6b..6a252e6f6fc 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -7,6 +7,7 @@ import Scope from '../../scopes/Scope'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../../utils/PathTracker'; import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; import ClassBody from '../ClassBody'; +import FunctionExpression from '../FunctionExpression'; import Identifier from '../Identifier'; import MethodDefinition from '../MethodDefinition'; import { ExpressionEntity } from './Expression'; @@ -21,6 +22,7 @@ export default class ClassNode extends NodeBase { private deoptimizedStatic = false; // Collect deoptimization information if we can resolve a property access, by property name private expressionsToBeDeoptimized = new Map(); + private propEffectTables: DynamicPropEffectsTable[] = [] // Known, simple, non-deoptimized static properties are kept in here. They are removed when deoptimized. private staticPropertyMap: {[name: string]: ExpressionNode} | null = null; @@ -117,11 +119,11 @@ export default class ClassNode extends NodeBase { if (this.deoptimizedPrototype) return true; const key2 = path[1]; if (path.length === 2 && typeof key2 === 'string') { - return mayHaveGetterSetterEffect(this.body, 'get', false, key2, context); + return this.mayHaveGetterSetterEffects('get', false, key2, context); } return true; } else if (typeof key === 'string' && path.length === 1) { - return mayHaveGetterSetterEffect(this.body, 'get', true, key, context); + return this.mayHaveGetterSetterEffects('get', true, key, context); } else { return true; } @@ -136,11 +138,11 @@ export default class ClassNode extends NodeBase { if (this.deoptimizedPrototype) return true; const key2 = path[1]; if (path.length === 2 && typeof key2 === 'string') { - return mayHaveGetterSetterEffect(this.body, 'set', false, key2, context); + return this.mayHaveGetterSetterEffects('set', false, key2, context); } return true; } else if (typeof key === 'string' && path.length === 1) { - return mayHaveGetterSetterEffect(this.body, 'set', true, key, context); + return this.mayHaveGetterSetterEffects('set', true, key, context); } else { return true; } @@ -179,6 +181,13 @@ export default class ClassNode extends NodeBase { this.classConstructor = null; } + mayHaveGetterSetterEffects(kind: 'get' | 'set', isStatic: boolean, name: string, context: HasEffectsContext) { + const key = (isStatic ? 1 : 0) + (kind == 'get' ? 2 : 0) + let table = this.propEffectTables[key] + if (!table) table = this.propEffectTables[key] = new DynamicPropEffectsTable(this.body, kind, isStatic) + return table.hasEffects(name, context) + } + mayModifyThisWhenCalledAtPath( path: ObjectPath ) { @@ -218,21 +227,30 @@ export default class ClassNode extends NodeBase { } } -function mayHaveGetterSetterEffect( - body: ClassBody, - kind: 'get' | 'set', isStatic: boolean, name: string, - context: HasEffectsContext -) { - for (const definition of body.body) { - if (definition instanceof MethodDefinition && definition.static === isStatic && definition.kind === kind) { - if (definition.computed || !(definition.key instanceof Identifier)) { - return true; - } - if (definition.key.name === name && - definition.value.hasEffectsWhenCalledAtPath([], {args: [], withNew: false}, context)) { - return true; +const defaultCallOptions = {args: [], withNew: false} + +class DynamicPropEffectsTable { + // Null means we should always assume effects + methods: {[name: string]: FunctionExpression[]} | null = Object.create(null) + + constructor(body: ClassBody, kind: 'get' | 'set', isStatic: boolean) { + for (const definition of body.body) { + if (definition instanceof MethodDefinition && + definition.static === isStatic && + definition.kind === kind) { + if (definition.computed || !(definition.key instanceof Identifier)) { + this.methods = null; + return + } + const name = definition.key.name; + (this.methods![name] || (this.methods![name] = [])).push(definition.value); } } } - return false; + + hasEffects(name: string, context: HasEffectsContext) { + if (!this.methods) return true; + const methods = this.methods[name]; + return methods && methods.some(m => m.hasEffectsWhenCalledAtPath([], defaultCallOptions, context)) + } } From cea520d1e123c06edb88d476781ea738ddae15cb Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 2 Apr 2021 14:45:53 +0200 Subject: [PATCH 05/50] Add comment explaining property map --- src/ast/nodes/ObjectExpression.ts | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 3b32620de48..34b5ef77aa6 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -28,6 +28,41 @@ import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; +/** + * This is a map of all properties of an object to allow quick lookups of relevant properties. These + * are the meanings of the properties: + * - exactMatchRead: If we know that there is at least one property with a given key, then this will + * contain the last property of that name in the object definition. "Read" means for reading + * access, i.e. this is the last regular property or getter for that name. Setters are ignored. + * Only if an "exactMatchRead" exists do we have a chance to e.g. get a literal value for that + * property. However, there is also a second property that is important here: + * - propertiesRead: This is an array that contains the "exactMatchRead", but also all computed + * properties that cannot be resolved and are define after the exactMatchRead in the object. + * Note that this value only has meaning if an "exactMatchRead" exists as otherwise there + * was no known readable property of that given name but only a setter. + * If it does not exist, then the instance property "unmatchablePropertiesRead" will contain all + * unresolved properties that might resolve to a given key. + * This property is important for deoptimization: If a property is mutated, all "possible + * matches" need to be deoptimized. + * - exactMatchWrite/propertiesWrite: Equivalent to exactMatchRead/propertiesRead except they only + * look at regular properties and setters but ignore getters + * + * Example: + * { + * foo: 'first', + * foo: 'second', + * [unknown]: 'third'; + * [otherUnknown]: 'fourth'; + * } + * + * In this case you get: + * { + * exactMatchRead: , + * propertiesRead: [, <[unknown]: 'third'>, <[otherUnknown]: 'fourth'>] + * exactMatchWrite: , + * propertiesWrite: [, <[unknown]: 'third'>, <[otherUnknown]: 'fourth'>] + * } + */ interface PropertyMap { [key: string]: { exactMatchRead: Property | null; From 1251f5a933b0499c29590c9a2c0ce814b310a2cd Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 15 Apr 2021 06:38:14 +0200 Subject: [PATCH 06/50] Fix types --- src/ast/nodes/shared/ClassNode.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 6a252e6f6fc..b89f3510014 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -189,12 +189,13 @@ export default class ClassNode extends NodeBase { } mayModifyThisWhenCalledAtPath( - path: ObjectPath + path: ObjectPath, + recursionTracker: PathTracker ) { const key = path[0]; const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; if (!definition || this.deoptimizedStatic) return true; - return definition.mayModifyThisWhenCalledAtPath(path.slice(1)); + return definition.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker); } private getStaticPropertyMap(): {[name: string]: ExpressionNode} { @@ -216,7 +217,7 @@ export default class ClassNode extends NodeBase { } seen[key] = true; if (definition instanceof MethodDefinition) { - if (definition.kind === "method") { + if (definition.kind === "method") { propertyMap[key] = definition.value; } } else if (definition.value) { From 056d9f06dad449c3a2bb94b4fe7a5e81cc2e899c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 17 Apr 2021 06:50:55 +0200 Subject: [PATCH 07/50] Make getReturnExpression and getLiteralValue more similar for objects --- src/ast/Entity.ts | 4 +- src/ast/nodes/CallExpression.ts | 1 - src/ast/nodes/ObjectExpression.ts | 106 +++++++++--------- .../_expected.js | 26 +++-- .../side-effects-getters-and-setters/main.js | 48 ++++---- 5 files changed, 93 insertions(+), 92 deletions(-) diff --git a/src/ast/Entity.ts b/src/ast/Entity.ts index f67667c74c4..8ed7c1b179e 100644 --- a/src/ast/Entity.ts +++ b/src/ast/Entity.ts @@ -1,9 +1,7 @@ import { HasEffectsContext } from './ExecutionContext'; import { ObjectPath } from './utils/PathTracker'; -export interface Entity { - toString: () => string; -} +export interface Entity {} export interface WritableEntity extends Entity { /** diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 4478a272612..49adeb2e6e7 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -70,7 +70,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } // ensure the returnExpression is set for the tree-shaking passes this.getReturnExpression(SHARED_RECURSION_TRACKER); - // This deoptimizes "this" for non-namespace calls until we have a better solution if ( this.callee instanceof MemberExpression && !this.callee.variable && diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index b25a7bd9631..61a8a3ee737 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -86,9 +86,9 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE private unmatchablePropertiesWrite: Property[] = []; bind() { - super.bind(); // ensure the propertyMap is set for the tree-shaking passes - this.getPropertyMap(); + this.buildPropertyMap(); + super.bind(); } // We could also track this per-property but this would quickly become much more complex @@ -98,7 +98,8 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE deoptimizePath(path: ObjectPath) { if (this.hasUnknownDeoptimizedProperty) return; - const propertyMap = this.getPropertyMap(); + this.buildPropertyMap(); + const propertyMap = this.propertyMap!; const key = path[0]; if (path.length === 1) { if (typeof key !== 'string') { @@ -133,7 +134,8 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - const propertyMap = this.getPropertyMap(); + this.buildPropertyMap(); + const propertyMap = this.propertyMap!; const key = path[0]; if ( @@ -146,29 +148,28 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } if ( - path.length === 1 && - !propertyMap[key] && - !objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 + propertyMap[key] && + propertyMap[key].exactMatchRead && + propertyMap[key].propertiesRead.length === 1 ) { getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return undefined; + return propertyMap[key].exactMatchRead!.getLiteralValueAtPath( + path.slice(1), + recursionTracker, + origin + ); } - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { + if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { return UnknownValue; } - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getLiteralValueAtPath( - path.slice(1), - recursionTracker, - origin - ); + if (path.length === 1 && !objectMembers[key]) { + getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); + return undefined; + } + + return UnknownValue; } getReturnExpressionWhenCalledAtPath( @@ -176,7 +177,8 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - const propertyMap = this.getPropertyMap(); + this.buildPropertyMap(); + const propertyMap = this.propertyMap!; const key = path[0]; if ( @@ -189,28 +191,27 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } if ( - path.length === 1 && - objectMembers[key] && - this.unmatchablePropertiesRead.length === 0 && - (!propertyMap[key] || propertyMap[key].exactMatchRead === null) + propertyMap[key] && + propertyMap[key].exactMatchRead && + propertyMap[key].propertiesRead.length === 1 ) { - return getMemberReturnExpressionWhenCalled(objectMembers, key); + getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); + return propertyMap[key].exactMatchRead!.getReturnExpressionWhenCalledAtPath( + path.slice(1), + recursionTracker, + origin + ); } - if ( - !propertyMap[key] || - propertyMap[key].exactMatchRead === null || - propertyMap[key].propertiesRead.length > 1 - ) { + if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { return UNKNOWN_EXPRESSION; } - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getReturnExpressionWhenCalledAtPath( - path.slice(1), - recursionTracker, - origin - ); + if (path.length === 1) { + return getMemberReturnExpressionWhenCalled(objectMembers, key); + } + + return UNKNOWN_EXPRESSION; } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { @@ -295,7 +296,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE if (!path.length || typeof path[0] !== 'string') { return true; } - const property = this.getPropertyMap()[path[0]]?.exactMatchRead; + const property = this.propertyMap![path[0]]?.exactMatchRead; return property ? property.value.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker) : true; @@ -317,21 +318,9 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private deoptimizeAllProperties() { - this.hasUnknownDeoptimizedProperty = true; - for (const property of this.properties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - private getPropertyMap(): PropertyMap { + private buildPropertyMap(): void { if (this.propertyMap !== null) { - return this.propertyMap; + return; } const propertyMap = (this.propertyMap = Object.create(null)); for (let index = this.properties.length - 1; index >= 0; index--) { @@ -384,6 +373,17 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE propertyMapProperty.propertiesWrite.push(property, ...this.unmatchablePropertiesWrite); } } - return propertyMap; + } + + private deoptimizeAllProperties() { + this.hasUnknownDeoptimizedProperty = true; + for (const property of this.properties) { + property.deoptimizePath(UNKNOWN_PATH); + } + for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } } } diff --git a/test/form/samples/side-effects-getters-and-setters/_expected.js b/test/form/samples/side-effects-getters-and-setters/_expected.js index 4100b6ca31b..31917077c8e 100644 --- a/test/form/samples/side-effects-getters-and-setters/_expected.js +++ b/test/form/samples/side-effects-getters-and-setters/_expected.js @@ -1,34 +1,36 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -retained1a.effect; -retained1a[ 'eff' + 'ect' ]; + +//retained +retained1.effect; +retained1['eff' + 'ect']; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index b3ce3d4a9b6..1bb8fd71c47 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -1,49 +1,51 @@ -const retained1a = { - get effect () { - console.log( 'effect' ); +const retained1 = { + get effect() { + console.log('effect'); }, - get noEffect () { + get noEffect() { const x = 1; return x; } }; -const removed1 = retained1a.noEffect; -const retained1b = retained1a.effect; -const retained1c = retained1a[ 'eff' + 'ect' ]; +// removed +retained1.noEffect; -const removed2a = { - get shadowedEffect () { - console.log( 'effect' ); +//retained +retained1.effect; +retained1['eff' + 'ect']; + +const removed2 = { + get shadowedEffect() { + console.log('effect'); return 1; }, shadowedEffect: true, - set shadowedEffect ( value ) { - console.log( value ); + set shadowedEffect(value) { + console.log(value); } }; -const removed2b = removed2a.shadowedEffect; -const removed2c = removed2a.missingProp; +removed2.shadowedEffect; const retained3 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; retained3.effect = 'retained'; const retained4 = { - set effect ( value ) { - console.log( value ); + set effect(value) { + console.log(value); } }; -retained4[ 'eff' + 'ect' ] = 'retained'; +retained4['eff' + 'ect'] = 'retained'; const removed5 = { - set noEffect ( value ) { + set noEffect(value) { const x = value; } }; @@ -51,8 +53,8 @@ const removed5 = { removed5.noEffect = 'removed'; const removed6 = { - set shadowedEffect ( value ) { - console.log( value ); + set shadowedEffect(value) { + console.log(value); }, shadowedEffect: true }; @@ -62,7 +64,7 @@ removed6.missingProp = true; const retained7 = { foo: () => {}, - get foo () { + get foo() { return 1; } }; From 9507110a2e557d43c972686975aa84714080ad5e Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 17 Apr 2021 07:20:45 +0200 Subject: [PATCH 08/50] Use common logic for return expression and literal value --- src/ast/nodes/CallExpression.ts | 2 +- src/ast/nodes/ObjectExpression.ts | 91 +++++++++++++++---------------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 49adeb2e6e7..f132ad3320d 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -89,7 +89,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; if (returnExpression !== UNKNOWN_EXPRESSION) { - // We need to replace here because is possible new expressions are added + // We need to replace here because it is possible new expressions are added // while we are deoptimizing the old ones this.expressionsToBeDeoptimized = []; if (this.wasPathDeoptmizedWhileOptimized) { diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 61a8a3ee737..6a356acd0a5 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -8,6 +8,7 @@ import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, + ObjectPathKey, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH @@ -134,41 +135,24 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - this.buildPropertyMap(); - const propertyMap = this.propertyMap!; - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { + if (path.length === 0) { return UnknownValue; } - if ( - propertyMap[key] && - propertyMap[key].exactMatchRead && - propertyMap[key].propertiesRead.length === 1 - ) { + const key = path[0]; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + if (expressionAtPath === UNKNOWN_EXPRESSION) { + return UnknownValue; + } getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getLiteralValueAtPath( - path.slice(1), - recursionTracker, - origin - ); + return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); } - if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { - return UnknownValue; - } - - if (path.length === 1 && !objectMembers[key]) { + if (path.length === 1 && !objectMembers[key as string]) { getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); return undefined; } - return UnknownValue; } @@ -177,40 +161,27 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - this.buildPropertyMap(); - const propertyMap = this.propertyMap!; - const key = path[0]; - - if ( - path.length === 0 || - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { + if (path.length === 0) { return UNKNOWN_EXPRESSION; } - if ( - propertyMap[key] && - propertyMap[key].exactMatchRead && - propertyMap[key].propertiesRead.length === 1 - ) { + const key = path[0]; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + if (expressionAtPath === UNKNOWN_EXPRESSION) { + return UNKNOWN_EXPRESSION; + } getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return propertyMap[key].exactMatchRead!.getReturnExpressionWhenCalledAtPath( + return expressionAtPath.getReturnExpressionWhenCalledAtPath( path.slice(1), recursionTracker, origin ); } - if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { - return UNKNOWN_EXPRESSION; - } - if (path.length === 1) { return getMemberReturnExpressionWhenCalled(objectMembers, key); } - return UNKNOWN_EXPRESSION; } @@ -386,4 +357,30 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } } + + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if ( + this.hasUnknownDeoptimizedProperty || + typeof key !== 'string' || + this.deoptimizedPaths.has(key) + ) { + return UNKNOWN_EXPRESSION; + } + + this.buildPropertyMap(); + const propertyMap = this.propertyMap!; + if ( + propertyMap[key] && + propertyMap[key].exactMatchRead && + propertyMap[key].propertiesRead.length === 1 + ) { + return propertyMap[key].exactMatchRead!; + } + + if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { + return UNKNOWN_EXPRESSION; + } + + return null; + } } From 7f51eefb81cb7466aba597bd52940e46e0d34ecd Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 17 Apr 2021 07:38:58 +0200 Subject: [PATCH 09/50] Use common logic for return access and call effects --- src/ast/nodes/ObjectExpression.ts | 139 ++++++++++++------------------ 1 file changed, 54 insertions(+), 85 deletions(-) diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 6a356acd0a5..5e2f397248a 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -86,12 +86,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE private unmatchablePropertiesRead: (Property | SpreadElement)[] = []; private unmatchablePropertiesWrite: Property[] = []; - bind() { - // ensure the propertyMap is set for the tree-shaking passes - this.buildPropertyMap(); - super.bind(); - } - // We could also track this per-property but this would quickly become much more complex deoptimizeCache() { if (!this.hasUnknownDeoptimizedProperty) this.deoptimizeAllProperties(); @@ -99,8 +93,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE deoptimizePath(path: ObjectPath) { if (this.hasUnknownDeoptimizedProperty) return; - this.buildPropertyMap(); - const propertyMap = this.propertyMap!; + const propertyMap = this.getPropertyMap(); const key = path[0]; if (path.length === 1) { if (typeof key !== 'string') { @@ -186,28 +179,15 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; + if (path.length === 0) { + return false; + } const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) || - !propertyMap[key] || - propertyMap[key].exactMatchRead === null) - ) - return true; - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : propertyMap[key] - ? propertyMap[key].propertiesRead - : []) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAccessedAtPath(path.slice(1), context); } - return false; + return path.length > 1; } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { @@ -242,25 +222,14 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE context: HasEffectsContext ): boolean { const key = path[0]; - if ( - typeof key !== 'string' || - this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key) || - (this.propertyMap![key] - ? !this.propertyMap![key].exactMatchRead - : path.length > 1 || !objectMembers[key]) - ) { - return true; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); } - const subPath = path.slice(1); - if (this.propertyMap![key]) { - for (const property of this.propertyMap![key].propertiesRead) { - if (property.hasEffectsWhenCalledAtPath(subPath, callOptions, context)) return true; - } + if (path.length > 1) { + return true; } - if (path.length === 1 && objectMembers[key]) - return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); - return false; + return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); } mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { @@ -289,9 +258,46 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private buildPropertyMap(): void { + private deoptimizeAllProperties() { + this.hasUnknownDeoptimizedProperty = true; + for (const property of this.properties) { + property.deoptimizePath(UNKNOWN_PATH); + } + for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if ( + this.hasUnknownDeoptimizedProperty || + typeof key !== 'string' || + this.deoptimizedPaths.has(key) + ) { + return UNKNOWN_EXPRESSION; + } + + const propertyMap = this.getPropertyMap(); + if ( + propertyMap[key] && + propertyMap[key].exactMatchRead && + propertyMap[key].propertiesRead.length === 1 + ) { + return propertyMap[key].exactMatchRead!; + } + + if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { + return UNKNOWN_EXPRESSION; + } + + return null; + } + + private getPropertyMap(): PropertyMap { if (this.propertyMap !== null) { - return; + return this.propertyMap; } const propertyMap = (this.propertyMap = Object.create(null)); for (let index = this.properties.length - 1; index >= 0; index--) { @@ -344,43 +350,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE propertyMapProperty.propertiesWrite.push(property, ...this.unmatchablePropertiesWrite); } } - } - - private deoptimizeAllProperties() { - this.hasUnknownDeoptimizedProperty = true; - for (const property of this.properties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { - if ( - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UNKNOWN_EXPRESSION; - } - - this.buildPropertyMap(); - const propertyMap = this.propertyMap!; - if ( - propertyMap[key] && - propertyMap[key].exactMatchRead && - propertyMap[key].propertiesRead.length === 1 - ) { - return propertyMap[key].exactMatchRead!; - } - - if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { - return UNKNOWN_EXPRESSION; - } - - return null; + return propertyMap; } } From 10c4e40f9864c861360f9c15c8f695634ad11529 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 19 Apr 2021 08:40:55 +0200 Subject: [PATCH 10/50] Extract shared logic from ObjectExpression --- src/ast/nodes/CallExpression.ts | 8 +- src/ast/nodes/Identifier.ts | 8 +- src/ast/nodes/MemberExpression.ts | 11 +- src/ast/nodes/ObjectExpression.ts | 244 +++++------------- src/ast/nodes/Property.ts | 11 + src/ast/nodes/shared/ClassNode.ts | 5 +- src/ast/nodes/shared/Expression.ts | 6 +- src/ast/nodes/shared/MultiExpression.ts | 10 +- src/ast/nodes/shared/Node.ts | 6 +- src/ast/utils/ObjectPathHandler.ts | 138 ++++++++++ src/ast/variables/LocalVariable.ts | 8 +- src/ast/variables/Variable.ts | 6 +- .../object-expression/_expected.js | 11 - .../return-expressions/_expected.js | 2 - test/form/samples/recursive-calls/main.js | 7 - .../modify-object-via-this-d/_config.js | 3 + .../samples/modify-object-via-this-d/main.js | 13 + 17 files changed, 278 insertions(+), 219 deletions(-) create mode 100644 src/ast/utils/ObjectPathHandler.ts create mode 100644 test/function/samples/modify-object-via-this-d/_config.js create mode 100644 test/function/samples/modify-object-via-this-d/main.js diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index f132ad3320d..da9362598a1 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -73,7 +73,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if ( this.callee instanceof MemberExpression && !this.callee.variable && - this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER) + this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER, this) ) { this.callee.object.deoptimizePath(UNKNOWN_PATH); } @@ -101,6 +101,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt expression.deoptimizeCache(); } } + if ( + this.callee instanceof MemberExpression && + !this.callee.variable + ) { + this.callee.object.deoptimizePath(UNKNOWN_PATH); + } } deoptimizePath(path: ObjectPath) { diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 0c1394198ef..9c5b787fa73 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -141,9 +141,13 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable!.includeCallArguments(context, args); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { return this.variable - ? this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker) + ? this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) : true; } diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 90ecfb4453a..aee336f1e2b 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -233,13 +233,18 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.propertyKey = getResolvablePropertyKey(this); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { if (this.variable) { - return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker); + return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } return this.object.mayModifyThisWhenCalledAtPath( [this.propertyKey as ObjectPathKey].concat(path), - recursionTracker + recursionTracker, + origin ); } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 5e2f397248a..c707286dbc1 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -1,17 +1,15 @@ import MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; -import { getOrCreate } from '../../utils/getOrCreate'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { getObjectPathHandler, ObjectPathHandler, PropertyMap } from '../utils/ObjectPathHandler'; import { EMPTY_PATH, ObjectPath, - ObjectPathKey, PathTracker, - SHARED_RECURSION_TRACKER, - UNKNOWN_PATH + SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import { getMemberReturnExpressionWhenCalled, @@ -29,98 +27,19 @@ import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; -/** - * This is a map of all properties of an object to allow quick lookups of relevant properties. These - * are the meanings of the properties: - * - exactMatchRead: If we know that there is at least one property with a given key, then this will - * contain the last property of that name in the object definition. "Read" means for reading - * access, i.e. this is the last regular property or getter for that name. Setters are ignored. - * Only if an "exactMatchRead" exists do we have a chance to e.g. get a literal value for that - * property. However, there is also a second property that is important here: - * - propertiesRead: This is an array that contains the "exactMatchRead", but also all computed - * properties that cannot be resolved and are define after the exactMatchRead in the object. - * Note that this value only has meaning if an "exactMatchRead" exists as otherwise there - * was no known readable property of that given name but only a setter. - * If it does not exist, then the instance property "unmatchablePropertiesRead" will contain all - * unresolved properties that might resolve to a given key. - * This property is important for deoptimization: If a property is mutated, all "possible - * matches" need to be deoptimized. - * - exactMatchWrite/propertiesWrite: Equivalent to exactMatchRead/propertiesRead except they only - * look at regular properties and setters but ignore getters - * - * Example: - * { - * foo: 'first', - * foo: 'second', - * [unknown]: 'third'; - * [otherUnknown]: 'fourth'; - * } - * - * In this case you get: - * { - * exactMatchRead: , - * propertiesRead: [, <[unknown]: 'third'>, <[otherUnknown]: 'fourth'>] - * exactMatchWrite: , - * propertiesWrite: [, <[unknown]: 'third'>, <[otherUnknown]: 'fourth'>] - * } - */ -interface PropertyMap { - [key: string]: { - exactMatchRead: Property | null; - exactMatchWrite: Property | null; - propertiesRead: (Property | SpreadElement)[]; - propertiesWrite: Property[]; - }; -} - export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; - private deoptimizedPaths = new Set(); + private expressionsToBeDeoptimizedByKey = new Map(); + private objectPathHandler: ObjectPathHandler | null = null; - // We collect deoptimization information if we can resolve a computed property access - private expressionsToBeDeoptimized = new Map(); - private hasUnknownDeoptimizedProperty = false; - private propertyMap: PropertyMap | null = null; - private unmatchablePropertiesRead: (Property | SpreadElement)[] = []; - private unmatchablePropertiesWrite: Property[] = []; - - // We could also track this per-property but this would quickly become much more complex deoptimizeCache() { - if (!this.hasUnknownDeoptimizedProperty) this.deoptimizeAllProperties(); + this.getObjectPathHandler().deoptimizeCache(); } deoptimizePath(path: ObjectPath) { - if (this.hasUnknownDeoptimizedProperty) return; - const propertyMap = this.getPropertyMap(); - const key = path[0]; - if (path.length === 1) { - if (typeof key !== 'string') { - this.deoptimizeAllProperties(); - return; - } - if (!this.deoptimizedPaths.has(key)) { - this.deoptimizedPaths.add(key); - - // we only deoptimizeCache exact matches as in all other cases, - // we do not return a literal value or return expression - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized.get(key); - if (expressionsToBeDeoptimized) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - } - const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); - for (const property of typeof key === 'string' - ? propertyMap[key] - ? propertyMap[key].propertiesRead - : [] - : this.properties) { - property.deoptimizePath(subPath); - } + this.getObjectPathHandler().deoptimizePath(path); } getLiteralValueAtPath( @@ -131,19 +50,15 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE if (path.length === 0) { return UnknownValue; } - const key = path[0]; - const expressionAtPath = this.getMemberExpression(key); + const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( + key, + origin + ); if (expressionAtPath) { - if (expressionAtPath === UNKNOWN_EXPRESSION) { - return UnknownValue; - } - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); } - if (path.length === 1 && !objectMembers[key as string]) { - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); return undefined; } return UnknownValue; @@ -157,21 +72,18 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE if (path.length === 0) { return UNKNOWN_EXPRESSION; } - const key = path[0]; - const expressionAtPath = this.getMemberExpression(key); + const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( + key, + origin + ); if (expressionAtPath) { - if (expressionAtPath === UNKNOWN_EXPRESSION) { - return UNKNOWN_EXPRESSION; - } - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); return expressionAtPath.getReturnExpressionWhenCalledAtPath( path.slice(1), recursionTracker, origin ); } - if (path.length === 1) { return getMemberReturnExpressionWhenCalled(objectMembers, key); } @@ -183,7 +95,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE return false; } const key = path[0]; - const expressionAtPath = this.getMemberExpression(key); + const expressionAtPath = this.getObjectPathHandler().getMemberExpression(key); if (expressionAtPath) { return expressionAtPath.hasEffectsWhenAccessedAtPath(path.slice(1), context); } @@ -191,29 +103,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { - const key = path[0]; - const propertyMap = this.propertyMap!; - if ( - path.length > 1 && - (this.hasUnknownDeoptimizedProperty || - this.deoptimizedPaths.has(key as string) || - !propertyMap[key as string] || - propertyMap[key as string].exactMatchRead === null) - ) { - return true; - } - - const subPath = path.slice(1); - for (const property of typeof key !== 'string' - ? this.properties - : path.length > 1 - ? propertyMap[key].propertiesRead - : propertyMap[key] - ? propertyMap[key].propertiesWrite - : []) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; - } - return false; + return this.getObjectPathHandler().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -222,7 +112,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE context: HasEffectsContext ): boolean { const key = path[0]; - const expressionAtPath = this.getMemberExpression(key); + const expressionAtPath = this.getObjectPathHandler().getMemberExpression(key); if (expressionAtPath) { return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); } @@ -232,14 +122,27 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - if (!path.length || typeof path[0] !== 'string') { - return true; + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { + if (path.length === 0) { + return false; } - const property = this.propertyMap![path[0]]?.exactMatchRead; - return property - ? property.value.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker) - : true; + const key = path[0]; + const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( + key, + origin + ); + if (expressionAtPath) { + return expressionAtPath.mayModifyThisWhenCalledAtPath( + path.slice(1), + recursionTracker, + origin + ); + } + return false; } render( @@ -258,52 +161,17 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private deoptimizeAllProperties() { - this.hasUnknownDeoptimizedProperty = true; - for (const property of this.properties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of this.expressionsToBeDeoptimized.values()) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { - if ( - this.hasUnknownDeoptimizedProperty || - typeof key !== 'string' || - this.deoptimizedPaths.has(key) - ) { - return UNKNOWN_EXPRESSION; - } - - const propertyMap = this.getPropertyMap(); - if ( - propertyMap[key] && - propertyMap[key].exactMatchRead && - propertyMap[key].propertiesRead.length === 1 - ) { - return propertyMap[key].exactMatchRead!; + private getObjectPathHandler(): ObjectPathHandler { + if (this.objectPathHandler !== null) { + return this.objectPathHandler; } - - if (propertyMap[key] || this.unmatchablePropertiesRead.length > 0) { - return UNKNOWN_EXPRESSION; - } - - return null; - } - - private getPropertyMap(): PropertyMap { - if (this.propertyMap !== null) { - return this.propertyMap; - } - const propertyMap = (this.propertyMap = Object.create(null)); + const propertyMap: PropertyMap = Object.create(null); + const unmatchablePropertiesRead: ExpressionEntity[] = []; + const unmatchablePropertiesWrite: ExpressionEntity[] = []; for (let index = this.properties.length - 1; index >= 0; index--) { const property = this.properties[index]; if (property instanceof SpreadElement) { - this.unmatchablePropertiesRead.push(property); + unmatchablePropertiesRead.push(property); continue; } const isWrite = property.kind !== 'get'; @@ -325,9 +193,9 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } if (unmatchable || (key === '__proto__' && !property.computed)) { if (isRead) { - this.unmatchablePropertiesRead.push(property); + unmatchablePropertiesRead.push(property); } else { - this.unmatchablePropertiesWrite.push(property); + unmatchablePropertiesWrite.push(property); } continue; } @@ -336,20 +204,28 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE propertyMap[key] = { exactMatchRead: isRead ? property : null, exactMatchWrite: isWrite ? property : null, - propertiesRead: isRead ? [property, ...this.unmatchablePropertiesRead] : [], - propertiesWrite: isWrite && !isRead ? [property, ...this.unmatchablePropertiesWrite] : [] + propertiesRead: isRead ? [property, ...unmatchablePropertiesRead] : [], + propertiesWrite: isWrite && !isRead ? [property, ...unmatchablePropertiesWrite] : [] }; continue; } if (isRead && propertyMapProperty.exactMatchRead === null) { propertyMapProperty.exactMatchRead = property; - propertyMapProperty.propertiesRead.push(property, ...this.unmatchablePropertiesRead); + propertyMapProperty.propertiesRead.push(property, ...unmatchablePropertiesRead); } - if (isWrite && !isRead && propertyMapProperty.exactMatchWrite === null) { + if (isWrite && propertyMapProperty.exactMatchWrite === null) { propertyMapProperty.exactMatchWrite = property; - propertyMapProperty.propertiesWrite.push(property, ...this.unmatchablePropertiesWrite); + if (!isRead) { + propertyMapProperty.propertiesWrite.push(property, ...unmatchablePropertiesWrite); + } } } - return propertyMap; + return (this.objectPathHandler = getObjectPathHandler( + propertyMap, + unmatchablePropertiesRead, + unmatchablePropertiesWrite, + this.properties, + this.expressionsToBeDeoptimizedByKey + )); } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 1cfb12233e1..ca601265829 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -144,6 +144,17 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P }; } + mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity): boolean { + if (this.kind === 'get') { + return this.getReturnExpression().mayModifyThisWhenCalledAtPath( + path, + recursionTracker, + origin + ); + } + return this.value.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); + } + render(code: MagicString, options: RenderOptions) { if (!this.shorthand) { this.key.render(code, options); diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index b89f3510014..6c2b3942f30 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -190,12 +190,13 @@ export default class ClassNode extends NodeBase { mayModifyThisWhenCalledAtPath( path: ObjectPath, - recursionTracker: PathTracker + recursionTracker: PathTracker, + origin: DeoptimizableEntity ) { const key = path[0]; const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; if (!definition || this.deoptimizedStatic) return true; - return definition.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker); + return definition.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker, origin); } private getStaticPropertyMap(): {[name: string]: ExpressionNode} { diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index eba5367420b..e579388baca 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -33,5 +33,9 @@ export interface ExpressionEntity extends WritableEntity { ): boolean; include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void; includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void; - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker): boolean; + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): boolean; } diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index f657c7e3454..2be9e81c6b3 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -74,7 +74,13 @@ export class MultiExpression implements ExpressionEntity { includeCallArguments(): void {} - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { - return this.expressions.some(e => e.mayModifyThisWhenCalledAtPath(path, recursionTracker)); + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { + return this.expressions.some(e => + e.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) + ); } } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 26fa04daf55..22f5f7fb0ba 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -232,7 +232,11 @@ export class NodeBase implements ExpressionNode { } } - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { + mayModifyThisWhenCalledAtPath( + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ) { return true; } diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts new file mode 100644 index 00000000000..6e980605f11 --- /dev/null +++ b/src/ast/utils/ObjectPathHandler.ts @@ -0,0 +1,138 @@ +import { getOrCreate } from '../../utils/getOrCreate'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; +import { HasEffectsContext } from '../ExecutionContext'; +import { ExpressionEntity } from '../nodes/shared/Expression'; +import { UNKNOWN_EXPRESSION } from '../values'; +import { ObjectPath, ObjectPathKey, UNKNOWN_PATH } from './PathTracker'; + +// TODO Lukas simplify +// TODO Lukas maybe an "intermediate format" that just contains objects {kind, key, prop}[], __proto__ could be an unshift {kind: "prop", key: UnknownKey, prop: UNKNOWN_EXPRESSION}? +// TODO Lukas propertiesWrite is currently actually "setters"; maybe it makes sense to also distinguish "getters" for easier access checking? +export interface PropertyMap { + [key: string]: { + exactMatchRead: ExpressionEntity | null; + exactMatchWrite: ExpressionEntity | null; + propertiesRead: ExpressionEntity[]; + propertiesWrite: ExpressionEntity[]; + }; +} + +export type ObjectPathHandler = ReturnType; + +// TODO Lukas use an object for expressionsToBeDeoptimizedByKey for better performance +export function getObjectPathHandler( + propertyMap: PropertyMap, + // TODO Lukas unify naming: ..forAssignment? + unmatchablePropertiesRead: ExpressionEntity[], + unmatchablePropertiesWrite: ExpressionEntity[], + allProperties: ExpressionEntity[], + expressionsToBeDeoptimizedByKey: Map +) { + let hasUnknownDeoptimizedProperty = false; + const deoptimizedPaths = new Set(); + + function getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if (hasUnknownDeoptimizedProperty || typeof key !== 'string' || deoptimizedPaths.has(key)) { + return UNKNOWN_EXPRESSION; + } + + if ( + propertyMap[key] && + propertyMap[key].exactMatchRead && + propertyMap[key].propertiesRead.length === 1 + ) { + return propertyMap[key].exactMatchRead!; + } + + if (propertyMap[key] || unmatchablePropertiesRead.length > 0) { + return UNKNOWN_EXPRESSION; + } + + return null; + } + + function getMemberExpressionAndTrackDeopt( + key: ObjectPathKey, + origin: DeoptimizableEntity + ): ExpressionEntity | null { + const expression = getMemberExpression(key); + if (expression !== UNKNOWN_EXPRESSION) { + getOrCreate(expressionsToBeDeoptimizedByKey, key, () => []).push(origin); + } + return expression; + } + + function hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + const expressionAtPath = getMemberExpression(key); + return !expressionAtPath || expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + } + + if (typeof key !== 'string') return true; + + const properties = propertyMap[key]?.exactMatchWrite + ? propertyMap[key].propertiesWrite + : unmatchablePropertiesWrite; + for (const property of properties) { + if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + return false; + } + + function deoptimizePath(path: ObjectPath) { + if (hasUnknownDeoptimizedProperty) return; + const key = path[0]; + if (path.length === 1) { + if (typeof key !== 'string') { + deoptimizeAllProperties(); + return; + } + if (!deoptimizedPaths.has(key)) { + deoptimizedPaths.add(key); + + // we only deoptimizeCache exact matches as in all other cases, + // we do not return a literal value or return expression + const expressionsToBeDeoptimized = expressionsToBeDeoptimizedByKey.get(key); + if (expressionsToBeDeoptimized) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + } + const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + for (const property of typeof key === 'string' + ? propertyMap[key] + ? propertyMap[key].propertiesRead.concat(propertyMap[key].propertiesWrite) + : [] + : allProperties) { + property.deoptimizePath(subPath); + } + } + + function deoptimizeAllProperties() { + hasUnknownDeoptimizedProperty = true; + for (const property of allProperties) { + property.deoptimizePath(UNKNOWN_PATH); + } + for (const expressionsToBeDeoptimized of expressionsToBeDeoptimizedByKey.values()) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + + function deoptimizeCache() { + if (hasUnknownDeoptimizedProperty) return; + deoptimizeAllProperties(); + } + + return { + deoptimizeCache, + deoptimizePath, + getMemberExpression, + getMemberExpressionAndTrackDeopt, + hasEffectsWhenAssignedAtPath + }; +} diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 6c3ec83133d..ee0f5030043 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -189,7 +189,11 @@ export default class LocalVariable extends Variable { this.calledFromTryStatement = true; } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker) { + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return true; } @@ -198,7 +202,7 @@ export default class LocalVariable extends Variable { return true; } trackedEntities.add(this.init); - const result = this.init.mayModifyThisWhenCalledAtPath(path, recursionTracker); + const result = this.init.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); trackedEntities.delete(this.init); return result; } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 9a3564ff4d7..9de050fb0c2 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -96,7 +96,11 @@ export default class Variable implements ExpressionEntity { markCalledFromTryStatement() {} - mayModifyThisWhenCalledAtPath(_path: ObjectPath, _recursionTracker: PathTracker) { + mayModifyThisWhenCalledAtPath( + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ) { return true; } diff --git a/test/form/samples/builtin-prototypes/object-expression/_expected.js b/test/form/samples/builtin-prototypes/object-expression/_expected.js index 95b55b099ae..e69de29bb2d 100644 --- a/test/form/samples/builtin-prototypes/object-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/object-expression/_expected.js @@ -1,11 +0,0 @@ -const object = {}; -object.propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ); -({}).propertyIsEnumerable( 'toString' ).valueOf(); - -({}).hasOwnProperty( 'toString' ).valueOf(); -({}).isPrototypeOf( {} ).valueOf(); -({}).propertyIsEnumerable( 'toString' ).valueOf(); -({}).toLocaleString().trim(); -({}).toString().trim(); -({}).valueOf(); diff --git a/test/form/samples/object-expression/return-expressions/_expected.js b/test/form/samples/object-expression/return-expressions/_expected.js index 0405706b107..3538f6d9b82 100644 --- a/test/form/samples/object-expression/return-expressions/_expected.js +++ b/test/form/samples/object-expression/return-expressions/_expected.js @@ -12,8 +12,6 @@ const z = { z.a()(); const v = {}; - -v.toString().charCodeAt(0); // removed v.toString().doesNotExist(0); // retained const w = { diff --git a/test/form/samples/recursive-calls/main.js b/test/form/samples/recursive-calls/main.js index 89bae2a7738..306972ec96b 100644 --- a/test/form/samples/recursive-calls/main.js +++ b/test/form/samples/recursive-calls/main.js @@ -17,13 +17,6 @@ const removed5 = { }; removed5.x; -const removed6 = { - get x () { - return globalThis.unknown ? removed6.x : () => {}; - } -}; -removed6.x(); - const removed7 = { get x () { return globalThis.unknown ? removed7.x : {}; diff --git a/test/function/samples/modify-object-via-this-d/_config.js b/test/function/samples/modify-object-via-this-d/_config.js new file mode 100644 index 00000000000..0c4423de923 --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks object modification via "this"' +}; diff --git a/test/function/samples/modify-object-via-this-d/main.js b/test/function/samples/modify-object-via-this-d/main.js new file mode 100644 index 00000000000..55adf0b053b --- /dev/null +++ b/test/function/samples/modify-object-via-this-d/main.js @@ -0,0 +1,13 @@ +const obj = { + modify() {} +}; + +obj.modify = modify; + +function modify() { + this.modified = true; +} + +obj.modify(); + +assert.strictEqual(obj.modified ? 'MODIFIED' : 'BROKEN', 'MODIFIED'); From 99a794d17876671dd35e11fe66a7e11611406c6f Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 19 Apr 2021 08:49:36 +0200 Subject: [PATCH 11/50] Use an object for better performance --- src/ast/nodes/ObjectExpression.ts | 4 +--- src/ast/utils/ObjectPathHandler.ts | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index c707286dbc1..aa5f4e15e84 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -31,7 +31,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; - private expressionsToBeDeoptimizedByKey = new Map(); private objectPathHandler: ObjectPathHandler | null = null; deoptimizeCache() { @@ -224,8 +223,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE propertyMap, unmatchablePropertiesRead, unmatchablePropertiesWrite, - this.properties, - this.expressionsToBeDeoptimizedByKey + this.properties )); } } diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts index 6e980605f11..c58e538570d 100644 --- a/src/ast/utils/ObjectPathHandler.ts +++ b/src/ast/utils/ObjectPathHandler.ts @@ -1,9 +1,8 @@ -import { getOrCreate } from '../../utils/getOrCreate'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; import { ExpressionEntity } from '../nodes/shared/Expression'; import { UNKNOWN_EXPRESSION } from '../values'; -import { ObjectPath, ObjectPathKey, UNKNOWN_PATH } from './PathTracker'; +import { ObjectPath, ObjectPathKey, UnknownKey, UNKNOWN_PATH } from './PathTracker'; // TODO Lukas simplify // TODO Lukas maybe an "intermediate format" that just contains objects {kind, key, prop}[], __proto__ could be an unshift {kind: "prop", key: UnknownKey, prop: UNKNOWN_EXPRESSION}? @@ -19,17 +18,17 @@ export interface PropertyMap { export type ObjectPathHandler = ReturnType; -// TODO Lukas use an object for expressionsToBeDeoptimizedByKey for better performance export function getObjectPathHandler( propertyMap: PropertyMap, - // TODO Lukas unify naming: ..forAssignment? unmatchablePropertiesRead: ExpressionEntity[], unmatchablePropertiesWrite: ExpressionEntity[], - allProperties: ExpressionEntity[], - expressionsToBeDeoptimizedByKey: Map + allProperties: ExpressionEntity[] ) { let hasUnknownDeoptimizedProperty = false; const deoptimizedPaths = new Set(); + const expressionsToBeDeoptimizedByKey: Record = Object.create( + null + ); function getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { if (hasUnknownDeoptimizedProperty || typeof key !== 'string' || deoptimizedPaths.has(key)) { @@ -55,9 +54,14 @@ export function getObjectPathHandler( key: ObjectPathKey, origin: DeoptimizableEntity ): ExpressionEntity | null { + if (key === UnknownKey) { + return UNKNOWN_EXPRESSION; + } const expression = getMemberExpression(key); if (expression !== UNKNOWN_EXPRESSION) { - getOrCreate(expressionsToBeDeoptimizedByKey, key, () => []).push(origin); + const expressionsToBeDeoptimized = (expressionsToBeDeoptimizedByKey[key] = + expressionsToBeDeoptimizedByKey[key] || []); + expressionsToBeDeoptimized.push(origin); } return expression; } @@ -93,7 +97,7 @@ export function getObjectPathHandler( // we only deoptimizeCache exact matches as in all other cases, // we do not return a literal value or return expression - const expressionsToBeDeoptimized = expressionsToBeDeoptimizedByKey.get(key); + const expressionsToBeDeoptimized = expressionsToBeDeoptimizedByKey[key]; if (expressionsToBeDeoptimized) { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -116,7 +120,7 @@ export function getObjectPathHandler( for (const property of allProperties) { property.deoptimizePath(UNKNOWN_PATH); } - for (const expressionsToBeDeoptimized of expressionsToBeDeoptimizedByKey.values()) { + for (const expressionsToBeDeoptimized of Object.values(expressionsToBeDeoptimizedByKey)) { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } From 72dc62bb2844231cbf70f2839b08a4a320cbc90c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 20 Apr 2021 08:51:31 +0200 Subject: [PATCH 12/50] Simplify handling for setters and other properties --- src/ast/nodes/ObjectExpression.ts | 74 +++++---------- src/ast/utils/ObjectPathHandler.ts | 95 ++++++++++++------- src/ast/utils/PathTracker.ts | 1 + .../side-effects-getters-and-setters/main.js | 10 -- .../_config.js | 3 +- 5 files changed, 91 insertions(+), 92 deletions(-) diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index aa5f4e15e84..6a618745061 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,12 +4,17 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { getObjectPathHandler, ObjectPathHandler, PropertyMap } from '../utils/ObjectPathHandler'; +import { + getObjectPathHandler, + ObjectPathHandler, + ObjectProperty +} from '../utils/ObjectPathHandler'; import { EMPTY_PATH, ObjectPath, PathTracker, - SHARED_RECURSION_TRACKER + SHARED_RECURSION_TRACKER, + UnknownKey } from '../utils/PathTracker'; import { getMemberReturnExpressionWhenCalled, @@ -160,70 +165,43 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } + // TODO Lukas Instead of resolved keys, push expressions as keys to further move logic out of here private getObjectPathHandler(): ObjectPathHandler { if (this.objectPathHandler !== null) { return this.objectPathHandler; } - const propertyMap: PropertyMap = Object.create(null); - const unmatchablePropertiesRead: ExpressionEntity[] = []; - const unmatchablePropertiesWrite: ExpressionEntity[] = []; - for (let index = this.properties.length - 1; index >= 0; index--) { - const property = this.properties[index]; + const properties: ObjectProperty[] = []; + for (const property of this.properties) { if (property instanceof SpreadElement) { - unmatchablePropertiesRead.push(property); + properties.push({ kind: 'init', key: UnknownKey, property }); continue; } - const isWrite = property.kind !== 'get'; - const isRead = property.kind !== 'set'; let key: string; - let unmatchable = false; if (property.computed) { const keyValue = property.key.getLiteralValueAtPath( EMPTY_PATH, SHARED_RECURSION_TRACKER, this ); - if (keyValue === UnknownValue) unmatchable = true; - key = String(keyValue); - } else if (property.key instanceof Identifier) { - key = property.key.name; - } else { - key = String((property.key as Literal).value); - } - if (unmatchable || (key === '__proto__' && !property.computed)) { - if (isRead) { - unmatchablePropertiesRead.push(property); + if (keyValue === UnknownValue) { + properties.push({ kind: property.kind, key: UnknownKey, property }); + continue; } else { - unmatchablePropertiesWrite.push(property); + key = String(keyValue); } - continue; - } - const propertyMapProperty = propertyMap[key]; - if (!propertyMapProperty) { - propertyMap[key] = { - exactMatchRead: isRead ? property : null, - exactMatchWrite: isWrite ? property : null, - propertiesRead: isRead ? [property, ...unmatchablePropertiesRead] : [], - propertiesWrite: isWrite && !isRead ? [property, ...unmatchablePropertiesWrite] : [] - }; - continue; - } - if (isRead && propertyMapProperty.exactMatchRead === null) { - propertyMapProperty.exactMatchRead = property; - propertyMapProperty.propertiesRead.push(property, ...unmatchablePropertiesRead); - } - if (isWrite && propertyMapProperty.exactMatchWrite === null) { - propertyMapProperty.exactMatchWrite = property; - if (!isRead) { - propertyMapProperty.propertiesWrite.push(property, ...unmatchablePropertiesWrite); + } else { + key = + property.key instanceof Identifier + ? property.key.name + : String((property.key as Literal).value); + // TODO Lukas how would setters and getters for __proto__ behave, ok to ignore them? + if (key === '__proto__' && property.kind === 'init') { + properties.unshift({ kind: 'init', key: UnknownKey, property: UNKNOWN_EXPRESSION }); + continue; } } + properties.push({ kind: property.kind, key, property }); } - return (this.objectPathHandler = getObjectPathHandler( - propertyMap, - unmatchablePropertiesRead, - unmatchablePropertiesWrite, - this.properties - )); + return (this.objectPathHandler = getObjectPathHandler(properties)); } } diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts index c58e538570d..815bda320e8 100644 --- a/src/ast/utils/ObjectPathHandler.ts +++ b/src/ast/utils/ObjectPathHandler.ts @@ -4,26 +4,25 @@ import { ExpressionEntity } from '../nodes/shared/Expression'; import { UNKNOWN_EXPRESSION } from '../values'; import { ObjectPath, ObjectPathKey, UnknownKey, UNKNOWN_PATH } from './PathTracker'; -// TODO Lukas simplify -// TODO Lukas maybe an "intermediate format" that just contains objects {kind, key, prop}[], __proto__ could be an unshift {kind: "prop", key: UnknownKey, prop: UNKNOWN_EXPRESSION}? -// TODO Lukas propertiesWrite is currently actually "setters"; maybe it makes sense to also distinguish "getters" for easier access checking? -export interface PropertyMap { - [key: string]: { - exactMatchRead: ExpressionEntity | null; - exactMatchWrite: ExpressionEntity | null; - propertiesRead: ExpressionEntity[]; - propertiesWrite: ExpressionEntity[]; - }; +export interface ObjectProperty { + key: ObjectPathKey; + kind: 'init' | 'set' | 'get'; + property: ExpressionEntity; } +type PropertyMap = Record; + export type ObjectPathHandler = ReturnType; -export function getObjectPathHandler( - propertyMap: PropertyMap, - unmatchablePropertiesRead: ExpressionEntity[], - unmatchablePropertiesWrite: ExpressionEntity[], - allProperties: ExpressionEntity[] -) { +export function getObjectPathHandler(properties: ObjectProperty[]) { + const { + allProperties, + propertiesByKey, + settersByKey, + unmatchableProperties, + unmatchableSetters + } = getPropertyMaps(properties); + let hasUnknownDeoptimizedProperty = false; const deoptimizedPaths = new Set(); const expressionsToBeDeoptimizedByKey: Record = Object.create( @@ -34,19 +33,12 @@ export function getObjectPathHandler( if (hasUnknownDeoptimizedProperty || typeof key !== 'string' || deoptimizedPaths.has(key)) { return UNKNOWN_EXPRESSION; } - - if ( - propertyMap[key] && - propertyMap[key].exactMatchRead && - propertyMap[key].propertiesRead.length === 1 - ) { - return propertyMap[key].exactMatchRead!; + if (propertiesByKey[key]?.length === 1) { + return propertiesByKey[key][0]; } - - if (propertyMap[key] || unmatchablePropertiesRead.length > 0) { + if (propertiesByKey[key] || unmatchableProperties.length > 0) { return UNKNOWN_EXPRESSION; } - return null; } @@ -75,9 +67,7 @@ export function getObjectPathHandler( if (typeof key !== 'string') return true; - const properties = propertyMap[key]?.exactMatchWrite - ? propertyMap[key].propertiesWrite - : unmatchablePropertiesWrite; + const properties = settersByKey[key] || unmatchableSetters; for (const property of properties) { if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; } @@ -106,10 +96,11 @@ export function getObjectPathHandler( } } const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + for (const property of typeof key === 'string' - ? propertyMap[key] - ? propertyMap[key].propertiesRead.concat(propertyMap[key].propertiesWrite) - : [] + ? (propertiesByKey[key] || unmatchableProperties).concat( + settersByKey[key] || unmatchableSetters + ) : allProperties) { property.deoptimizePath(subPath); } @@ -140,3 +131,43 @@ export function getObjectPathHandler( hasEffectsWhenAssignedAtPath }; } + +function getPropertyMaps( + properties: ObjectProperty[] +): { + allProperties: ExpressionEntity[]; + propertiesByKey: Record; + settersByKey: Record; + unmatchableProperties: ExpressionEntity[]; + unmatchableSetters: ExpressionEntity[]; +} { + const allProperties = []; + const propertiesByKey: PropertyMap = Object.create(null); + const settersByKey: PropertyMap = Object.create(null); + const unmatchableProperties: ExpressionEntity[] = []; + const unmatchableSetters: ExpressionEntity[] = []; + for (let index = properties.length - 1; index >= 0; index--) { + const { key, kind, property } = properties[index]; + allProperties.push(property); + if (kind === 'set') { + if (typeof key !== 'string') { + unmatchableSetters.push(property); + } else if (!settersByKey[key]) { + settersByKey[key] = [property, ...unmatchableSetters]; + } + } else { + if (typeof key !== 'string') { + unmatchableProperties.push(property); + } else if (!propertiesByKey[key]) { + propertiesByKey[key] = [property, ...unmatchableProperties]; + } + } + } + return { + allProperties, + propertiesByKey, + settersByKey, + unmatchableProperties, + unmatchableSetters + }; +} diff --git a/src/ast/utils/PathTracker.ts b/src/ast/utils/PathTracker.ts index 74e10553744..06a726d5d85 100644 --- a/src/ast/utils/PathTracker.ts +++ b/src/ast/utils/PathTracker.ts @@ -18,6 +18,7 @@ interface EntityPaths { export class PathTracker { entityPaths: EntityPaths = Object.create(null, { [EntitiesKey]: { value: new Set() } }); + // TODO Lukas can we incorporate the usual usage patterns here? getEntities(path: ObjectPath): Set { let currentPaths = this.entityPaths; for (const pathSegment of path) { diff --git a/test/form/samples/side-effects-getters-and-setters/main.js b/test/form/samples/side-effects-getters-and-setters/main.js index 1bb8fd71c47..66cb31f8451 100644 --- a/test/form/samples/side-effects-getters-and-setters/main.js +++ b/test/form/samples/side-effects-getters-and-setters/main.js @@ -52,16 +52,6 @@ const removed5 = { removed5.noEffect = 'removed'; -const removed6 = { - set shadowedEffect(value) { - console.log(value); - }, - shadowedEffect: true -}; - -removed6.shadowedEffect = true; -removed6.missingProp = true; - const retained7 = { foo: () => {}, get foo() { diff --git a/test/form/samples/side-effects-object-literal-mutation/_config.js b/test/form/samples/side-effects-object-literal-mutation/_config.js index 2eaf74d0b0b..ffc52c259c1 100644 --- a/test/form/samples/side-effects-object-literal-mutation/_config.js +++ b/test/form/samples/side-effects-object-literal-mutation/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects side-effects when mutating object literals', - options: { output: { name: 'bundle' } } + description: 'detects side-effects when mutating object literals' }; From ba6e0433daf13bd4625e795c6ca525a29263cfb7 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 20 Apr 2021 09:09:10 +0200 Subject: [PATCH 13/50] Small simplification --- src/ast/nodes/ObjectExpression.ts | 4 +--- src/ast/utils/ObjectPathHandler.ts | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 6a618745061..12934020df3 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -39,7 +39,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE private objectPathHandler: ObjectPathHandler | null = null; deoptimizeCache() { - this.getObjectPathHandler().deoptimizeCache(); + this.getObjectPathHandler().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { @@ -165,7 +165,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - // TODO Lukas Instead of resolved keys, push expressions as keys to further move logic out of here private getObjectPathHandler(): ObjectPathHandler { if (this.objectPathHandler !== null) { return this.objectPathHandler; @@ -194,7 +193,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE property.key instanceof Identifier ? property.key.name : String((property.key as Literal).value); - // TODO Lukas how would setters and getters for __proto__ behave, ok to ignore them? if (key === '__proto__' && property.kind === 'init') { properties.unshift({ kind: 'init', key: UnknownKey, property: UNKNOWN_EXPRESSION }); continue; diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts index 815bda320e8..a930d0ad47b 100644 --- a/src/ast/utils/ObjectPathHandler.ts +++ b/src/ast/utils/ObjectPathHandler.ts @@ -107,6 +107,7 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { } function deoptimizeAllProperties() { + if (hasUnknownDeoptimizedProperty) return; hasUnknownDeoptimizedProperty = true; for (const property of allProperties) { property.deoptimizePath(UNKNOWN_PATH); @@ -118,13 +119,8 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { } } - function deoptimizeCache() { - if (hasUnknownDeoptimizedProperty) return; - deoptimizeAllProperties(); - } - return { - deoptimizeCache, + deoptimizeAllProperties, deoptimizePath, getMemberExpression, getMemberExpressionAndTrackDeopt, From f19fc61d4cbe9f3bc6869a090374fbccd7485b24 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 21 Apr 2021 07:00:00 +0200 Subject: [PATCH 14/50] Work towards better class handling --- src/ast/nodes/MethodDefinition.ts | 67 +++++++- src/ast/nodes/ObjectExpression.ts | 105 ++---------- src/ast/nodes/Property.ts | 28 ++- src/ast/nodes/PropertyDefinition.ts | 46 +++++ src/ast/nodes/shared/ClassNode.ts | 247 ++++++++------------------- src/ast/nodes/shared/ObjectEntity.ts | 139 +++++++++++++++ src/ast/utils/ObjectPathHandler.ts | 34 ++++ src/ast/values.ts | 1 + 8 files changed, 386 insertions(+), 281 deletions(-) create mode 100644 src/ast/nodes/shared/ObjectEntity.ts diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index 7ce5195cc2a..546898ba8d6 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -1,9 +1,12 @@ -import { CallOptions } from '../CallOptions'; +import { CallOptions, NO_ARGS } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../values'; import FunctionExpression from './FunctionExpression'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class MethodDefinition extends NodeBase { @@ -14,10 +17,50 @@ export default class MethodDefinition extends NodeBase { type!: NodeType.tMethodDefinition; value!: FunctionExpression; + private accessedValue: ExpressionEntity | null = null; + private accessorCallOptions: CallOptions = { + args: NO_ARGS, + withNew: false + }; + + deoptimizePath(path: ObjectPath) { + this.getAccessedValue().deoptimizePath(path); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getAccessedValue().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( + path, + recursionTracker, + origin + ); + } + hasEffects(context: HasEffectsContext) { return this.key.hasEffects(context); } + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'get') { + return ( + this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || + (path.length > 0 && this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context)) + ); + } + return this.value.hasEffectsWhenAccessedAtPath(path); + } + hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, @@ -27,4 +70,24 @@ export default class MethodDefinition extends NodeBase { path.length > 0 || this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) ); } + + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): boolean { + return this.getAccessedValue().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); + } + + private getAccessedValue(): ExpressionEntity { + if (this.accessedValue === null) { + if (this.kind === 'get') { + this.accessedValue = UNKNOWN_EXPRESSION; + this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH); + } else { + this.accessedValue = this.value; + } + } + return this.accessedValue; + } } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 12934020df3..c3b6af27b91 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,11 +4,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { - getObjectPathHandler, - ObjectPathHandler, - ObjectProperty -} from '../utils/ObjectPathHandler'; +import { ObjectProperty } from '../utils/ObjectPathHandler'; import { EMPTY_PATH, ObjectPath, @@ -16,34 +12,28 @@ import { SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; -import { - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - objectMembers, - UnknownValue, - UNKNOWN_EXPRESSION -} from '../values'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import Property from './Property'; import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; +import { ObjectEntity } from './shared/ObjectEntity'; import SpreadElement from './SpreadElement'; export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; - private objectPathHandler: ObjectPathHandler | null = null; + private objectEntity: ObjectEntity | null = null; deoptimizeCache() { - this.getObjectPathHandler().deoptimizeAllProperties(); + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { - this.getObjectPathHandler().deoptimizePath(path); + this.getObjectEntity().deoptimizePath(path); } getLiteralValueAtPath( @@ -51,21 +41,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (path.length === 0) { - return UnknownValue; - } - const key = path[0]; - const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( - key, - origin - ); - if (expressionAtPath) { - return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); - } - if (path.length === 1 && !objectMembers[key as string]) { - return undefined; - } - return UnknownValue; + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } getReturnExpressionWhenCalledAtPath( @@ -73,41 +49,19 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - if (path.length === 0) { - return UNKNOWN_EXPRESSION; - } - const key = path[0]; - const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( - key, + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + recursionTracker, origin ); - if (expressionAtPath) { - return expressionAtPath.getReturnExpressionWhenCalledAtPath( - path.slice(1), - recursionTracker, - origin - ); - } - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(objectMembers, key); - } - return UNKNOWN_EXPRESSION; } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) { - return false; - } - const key = path[0]; - const expressionAtPath = this.getObjectPathHandler().getMemberExpression(key); - if (expressionAtPath) { - return expressionAtPath.hasEffectsWhenAccessedAtPath(path.slice(1), context); - } - return path.length > 1; + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { - return this.getObjectPathHandler().hasEffectsWhenAssignedAtPath(path, context); + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -115,15 +69,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE callOptions: CallOptions, context: HasEffectsContext ): boolean { - const key = path[0]; - const expressionAtPath = this.getObjectPathHandler().getMemberExpression(key); - if (expressionAtPath) { - return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); - } - if (path.length > 1) { - return true; - } - return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } mayModifyThisWhenCalledAtPath( @@ -131,22 +77,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - if (path.length === 0) { - return false; - } - const key = path[0]; - const expressionAtPath = this.getObjectPathHandler().getMemberExpressionAndTrackDeopt( - key, - origin - ); - if (expressionAtPath) { - return expressionAtPath.mayModifyThisWhenCalledAtPath( - path.slice(1), - recursionTracker, - origin - ); - } - return false; + return this.getObjectEntity().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } render( @@ -165,9 +96,9 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } } - private getObjectPathHandler(): ObjectPathHandler { - if (this.objectPathHandler !== null) { - return this.objectPathHandler; + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; } const properties: ObjectProperty[] = []; for (const property of this.properties) { @@ -200,6 +131,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } properties.push({ kind: property.kind, key, property }); } - return (this.objectPathHandler = getObjectPathHandler(properties)); + return (this.objectEntity = new ObjectEntity(properties)); } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index ca601265829..ee8d4af68cc 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -26,16 +26,15 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P type!: NodeType.tProperty; value!: ExpressionNode | (ExpressionNode & PatternNode); - private accessorCallOptions!: CallOptions; + private accessorCallOptions: CallOptions = { + args: NO_ARGS, + withNew: false + }; private declarationInit: ExpressionEntity | null = null; private returnExpression: ExpressionEntity | null = null; bind() { super.bind(); - if (this.kind === 'get') { - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(); - } if (this.declarationInit !== null) { this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); } @@ -46,8 +45,8 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); } - // As getter properties directly receive their values from function expressions that always - // have a fixed return value, there is no known situation where a getter is deoptimized. + // As getter properties directly receive their values from fixed function + // expressions, there is no known situation where a getter is deoptimized. deoptimizeCache(): void {} deoptimizePath(path: ObjectPath) { @@ -91,6 +90,8 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P this.value.hasEffects(context); } + // TODO Lukas why do we have recursion tracking here? + // TODO Lukas can we simplify things like with MethodDefinition? hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (this.kind === 'get') { const trackedExpressions = context.accessed.getEntities(path); @@ -98,7 +99,7 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P trackedExpressions.add(this); return ( this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - (path.length > 0 && this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context)) + (path.length > 0 && this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context)) ); } return this.value.hasEffectsWhenAccessedAtPath(path, context); @@ -109,7 +110,7 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P const trackedExpressions = context.assigned.getEntities(path); if (trackedExpressions.has(this)) return false; trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); + return this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context); } if (this.kind === 'set') { const trackedExpressions = context.assigned.getEntities(path); @@ -132,18 +133,11 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P ).getEntities(path, callOptions); if (trackedExpressions.has(this)) return false; trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); + return this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context); } return this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); } - initialise() { - this.accessorCallOptions = { - args: NO_ARGS, - withNew: false - }; - } - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity): boolean { if (this.kind === 'get') { return this.getReturnExpression().mayModifyThisWhenCalledAtPath( diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 0b0b02e3643..80964fb743b 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,6 +1,11 @@ +import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { @@ -10,10 +15,51 @@ export default class PropertyDefinition extends NodeBase { type!: NodeType.tPropertyDefinition; value!: ExpressionNode | null; + deoptimizePath(path: ObjectPath) { + this.value?.deoptimizePath(path); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.value + ? this.value.getLiteralValueAtPath(path, recursionTracker, origin) + : UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.value + ? this.value.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + : UNKNOWN_EXPRESSION; + } + hasEffects(context: HasEffectsContext): boolean { return ( this.key.hasEffects(context) || (this.static && this.value !== null && this.value.hasEffects(context)) ); } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + // TODO Lukas verify this is modifying this + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): boolean { + return this.value?.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) || false; + } } diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 6c2b3942f30..ab16a4dbb8d 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,170 +1,94 @@ -import { getOrCreate } from '../../../utils/getOrCreate'; import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { EMPTY_PATH, ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; +import { ObjectProperty } from '../../utils/ObjectPathHandler'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UnknownKey +} from '../../utils/PathTracker'; +import { LiteralValueOrUnknown, UnknownValue } from '../../values'; import ClassBody from '../ClassBody'; -import FunctionExpression from '../FunctionExpression'; import Identifier from '../Identifier'; +import Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; import { ExpressionEntity } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; +import { ObjectEntity } from './ObjectEntity'; -export default class ClassNode extends NodeBase { +export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; id!: Identifier | null; superClass!: ExpressionNode | null; - private classConstructor!: MethodDefinition | null; - private deoptimizedPrototype = false; - private deoptimizedStatic = false; - // Collect deoptimization information if we can resolve a property access, by property name - private expressionsToBeDeoptimized = new Map(); - private propEffectTables: DynamicPropEffectsTable[] = [] - // Known, simple, non-deoptimized static properties are kept in here. They are removed when deoptimized. - private staticPropertyMap: {[name: string]: ExpressionNode} | null = null; - bind() { - super.bind(); - } + private classConstructor!: MethodDefinition | null; + private objectEntity: ObjectEntity | null = null; createScope(parentScope: Scope) { this.scope = new ChildScope(parentScope); } - deoptimizeAllStatics() { - for (const name in this.staticPropertyMap) { - this.deoptimizeStatic(name); - } - this.deoptimizedStatic = this.deoptimizedPrototype = true; + deoptimizeCache() { + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { - const propertyMap = this.getStaticPropertyMap(); - const key = path[0]; - const definition = typeof key === 'string' && propertyMap[key]; - if (path.length === 1) { - if (definition) { - this.deoptimizeStatic(key as string); - } else if (typeof key !== 'string') { - this.deoptimizeAllStatics(); - } - } else if (key === 'prototype' && typeof path[1] !== 'string') { - this.deoptimizedPrototype = true; - } else if (path.length > 1 && definition) { - definition.deoptimizePath(path.slice(1)); - } - } - - deoptimizeStatic(name: string) { - delete this.staticPropertyMap![name]; - const deoptEntities = this.expressionsToBeDeoptimized.get(name); - if (deoptEntities) { - for (const entity of deoptEntities) { - entity.deoptimizeCache(); - } - } + this.getObjectEntity().deoptimizePath(path); } + // TODO Lukas also check super class, prototype getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - const key = path[0]; - const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; - if (path.length === 0 || !definition || - (key === 'prototype' ? this.deoptimizedPrototype : this.deoptimizedStatic)) { - return UnknownValue; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return definition.getLiteralValueAtPath( - path.slice(1), - recursionTracker, - origin - ); + return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } + // TODO Lukas also check super class getReturnExpressionWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - const key = path[0]; - const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; - - if (path.length === 0 || !definition || - (key === 'prototype' ? this.deoptimizedPrototype : this.deoptimizedStatic)) { - return UNKNOWN_EXPRESSION; - } - - getOrCreate(this.expressionsToBeDeoptimized, key, () => []).push(origin); - return definition.getReturnExpressionWhenCalledAtPath( - path.slice(1), + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, recursionTracker, origin ); } + // TODO Lukas also check super class hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; - if (this.deoptimizedStatic) return true; - if (this.superClass && this.superClass.hasEffectsWhenAccessedAtPath(path, context)) return true; - const key = path[0]; - if (key === 'prototype') { - if (path.length === 1) return false; - if (this.deoptimizedPrototype) return true; - const key2 = path[1]; - if (path.length === 2 && typeof key2 === 'string') { - return this.mayHaveGetterSetterEffects('get', false, key2, context); - } - return true; - } else if (typeof key === 'string' && path.length === 1) { - return this.mayHaveGetterSetterEffects('get', true, key, context); - } else { - return true; - } + // TODO Lukas if there is no direct match and no effect, also check superclass + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } + // TODO Lukas prototype hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (this.deoptimizedStatic) return true; - if (this.superClass && this.superClass.hasEffectsWhenAssignedAtPath(path, context)) return true; - const key = path[0]; - if (key === 'prototype') { - if (path.length === 1) return false; - if (this.deoptimizedPrototype) return true; - const key2 = path[1]; - if (path.length === 2 && typeof key2 === 'string') { - return this.mayHaveGetterSetterEffects('set', false, key2, context); - } - return true; - } else if (typeof key === 'string' && path.length === 1) { - return this.mayHaveGetterSetterEffects('set', true, key, context); - } else { - return true; - } + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } + // TODO Lukas also check super class hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, context: HasEffectsContext ) { - if (callOptions.withNew) { - return path.length > 0 || - (this.classConstructor !== null && - this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context)) || - (this.superClass !== null && - this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)); + if (path.length === 0) { + return ( + !callOptions.withNew || + (this.classConstructor !== null + ? this.classConstructor.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) + : this.superClass !== null && + this.superClass.hasEffectsWhenCalledAtPath(path, callOptions, context)) + ); } else { - if (path.length !== 1 || this.deoptimizedStatic) return true; - const key = path[0]; - const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; - if (!definition) return true; - return definition.hasEffectsWhenCalledAtPath([], callOptions, context); + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } } @@ -181,78 +105,51 @@ export default class ClassNode extends NodeBase { this.classConstructor = null; } - mayHaveGetterSetterEffects(kind: 'get' | 'set', isStatic: boolean, name: string, context: HasEffectsContext) { - const key = (isStatic ? 1 : 0) + (kind == 'get' ? 2 : 0) - let table = this.propEffectTables[key] - if (!table) table = this.propEffectTables[key] = new DynamicPropEffectsTable(this.body, kind, isStatic) - return table.hasEffects(name, context) - } - + // TODO Lukas also check super class mayModifyThisWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - const key = path[0]; - const definition = typeof key === 'string' && this.getStaticPropertyMap()[key]; - if (!definition || this.deoptimizedStatic) return true; - return definition.mayModifyThisWhenCalledAtPath(path.slice(1), recursionTracker, origin); + return this.getObjectEntity().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } - private getStaticPropertyMap(): {[name: string]: ExpressionNode} { - if (this.staticPropertyMap) return this.staticPropertyMap; - - const propertyMap = this.staticPropertyMap = Object.create(null); - const seen: {[name: string]: boolean} = Object.create(null); - for (const definition of this.body.body) { - if (!definition.static) continue; - // If there are non-identifier-named statics, give up. - if (definition.computed || !(definition.key instanceof Identifier)) { - return this.staticPropertyMap = Object.create(null); - } - const key = definition.key.name; - // Not handling duplicate definitions. - if (seen[key]) { - delete propertyMap[key]; - continue; - } - seen[key] = true; - if (definition instanceof MethodDefinition) { - if (definition.kind === "method") { - propertyMap[key] = definition.value; - } - } else if (definition.value) { - propertyMap[key] = definition.value; - } + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; } - return this.staticPropertyMap = propertyMap; - } -} - -const defaultCallOptions = {args: [], withNew: false} - -class DynamicPropEffectsTable { - // Null means we should always assume effects - methods: {[name: string]: FunctionExpression[]} | null = Object.create(null) - - constructor(body: ClassBody, kind: 'get' | 'set', isStatic: boolean) { - for (const definition of body.body) { - if (definition instanceof MethodDefinition && - definition.static === isStatic && - definition.kind === kind) { - if (definition.computed || !(definition.key instanceof Identifier)) { - this.methods = null; - return + const staticProperties: ObjectProperty[] = []; + const dynamicProperties: ObjectProperty[] = []; + for (const definition of this.body.body) { + const properties = definition.static ? staticProperties : dynamicProperties; + const definitionKind = (definition as MethodDefinition | { kind: undefined }).kind; + const kind = definitionKind === 'set' || definitionKind === 'get' ? definitionKind : 'init'; + let key: string; + if (definition.computed) { + const keyValue = definition.key.getLiteralValueAtPath( + EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this + ); + if (keyValue === UnknownValue) { + properties.push({ kind, key: UnknownKey, property: definition }); + continue; + } else { + key = String(keyValue); } - const name = definition.key.name; - (this.methods![name] || (this.methods![name] = [])).push(definition.value); + } else { + key = + definition.key instanceof Identifier + ? definition.key.name + : String((definition.key as Literal).value); } + properties.push({ kind, key, property: definition }); } - } - - hasEffects(name: string, context: HasEffectsContext) { - if (!this.methods) return true; - const methods = this.methods[name]; - return methods && methods.some(m => m.hasEffectsWhenCalledAtPath([], defaultCallOptions, context)) + staticProperties.unshift({ + key: 'prototype', + kind: 'init', + property: new ObjectEntity(dynamicProperties) + }); + return (this.objectEntity = new ObjectEntity(staticProperties)); } } diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts new file mode 100644 index 00000000000..de618cf5510 --- /dev/null +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -0,0 +1,139 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { getObjectPathHandler, ObjectProperty } from '../../utils/ObjectPathHandler'; +import { ObjectPath, ObjectPathKey, PathTracker } from '../../utils/PathTracker'; +import { + getMemberReturnExpressionWhenCalled, + hasMemberEffectWhenCalled, + LiteralValueOrUnknown, + objectMembers, + UnknownValue, + UNKNOWN_EXPRESSION +} from '../../values'; +import SpreadElement from '../SpreadElement'; +import { ExpressionEntity } from './Expression'; +import { ExpressionNode } from './Node'; + +export class ObjectEntity implements ExpressionEntity { + deoptimizeAllProperties: () => void; + deoptimizePath: (path: ObjectPath) => void; + hasEffectsWhenAccessedAtPath: (path: ObjectPath, context: HasEffectsContext) => boolean; + hasEffectsWhenAssignedAtPath: (path: ObjectPath, context: HasEffectsContext) => boolean; + included = false; + + private getMemberExpression: (key: ObjectPathKey) => ExpressionEntity | null; + private getMemberExpressionAndTrackDeopt: ( + key: ObjectPathKey, + origin: DeoptimizableEntity + ) => ExpressionEntity | null; + + // TODO Lukas make this a proper class that we can extend? + constructor(properties: ObjectProperty[]) { + ({ + deoptimizeAllProperties: this.deoptimizeAllProperties, + deoptimizePath: this.deoptimizePath, + getMemberExpression: this.getMemberExpression, + getMemberExpressionAndTrackDeopt: this.getMemberExpressionAndTrackDeopt, + hasEffectsWhenAccessedAtPath: this.hasEffectsWhenAccessedAtPath, + hasEffectsWhenAssignedAtPath: this.hasEffectsWhenAssignedAtPath + } = getObjectPathHandler(properties)); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + if (path.length === 0) { + return UnknownValue; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt( + key, + origin + ); + if (expressionAtPath) { + return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); + } + if (path.length === 1 && !objectMembers[key as string]) { + return undefined; + } + return UnknownValue; + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + if (path.length === 0) { + return UNKNOWN_EXPRESSION; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt( + key, + origin + ); + if (expressionAtPath) { + return expressionAtPath.getReturnExpressionWhenCalledAtPath( + path.slice(1), + recursionTracker, + origin + ); + } + if (path.length === 1) { + return getMemberReturnExpressionWhenCalled(objectMembers, key); + } + return UNKNOWN_EXPRESSION; + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + const key = path[0]; + const expressionAtPath = this.getMemberExpression(key); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); + } + if (path.length > 1) { + return true; + } + return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); + } + + include() { + this.included = true; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { + for (const arg of args) { + arg.include(context, false); + } + } + + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ) { + if (path.length === 0) { + return false; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt( + key, + origin + ); + if (expressionAtPath) { + return expressionAtPath.mayModifyThisWhenCalledAtPath( + path.slice(1), + recursionTracker, + origin + ); + } + return false; + } +} diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts index a930d0ad47b..eae5b0d22ce 100644 --- a/src/ast/utils/ObjectPathHandler.ts +++ b/src/ast/utils/ObjectPathHandler.ts @@ -18,7 +18,9 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { const { allProperties, propertiesByKey, + gettersByKey, settersByKey, + unmatchableGetters, unmatchableProperties, unmatchableSetters } = getPropertyMaps(properties); @@ -29,6 +31,8 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { null ); + // TODO Lukas it would be really interesting to know if we have any getters here? + // -> Add a third table of getters here! function getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { if (hasUnknownDeoptimizedProperty || typeof key !== 'string' || deoptimizedPaths.has(key)) { return UNKNOWN_EXPRESSION; @@ -58,6 +62,22 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { return expression; } + function hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + const expressionAtPath = getMemberExpression(key); + return !expressionAtPath || expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); + } + + if (typeof key !== 'string') return true; + + const properties = gettersByKey[key] || unmatchableGetters; + for (const property of properties) { + if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + return false; + } + function hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const [key, ...subPath] = path; if (path.length > 1) { @@ -124,6 +144,7 @@ export function getObjectPathHandler(properties: ObjectProperty[]) { deoptimizePath, getMemberExpression, getMemberExpressionAndTrackDeopt, + hasEffectsWhenAccessedAtPath, hasEffectsWhenAssignedAtPath }; } @@ -132,16 +153,20 @@ function getPropertyMaps( properties: ObjectProperty[] ): { allProperties: ExpressionEntity[]; + gettersByKey: Record; propertiesByKey: Record; settersByKey: Record; + unmatchableGetters: ExpressionEntity[]; unmatchableProperties: ExpressionEntity[]; unmatchableSetters: ExpressionEntity[]; } { const allProperties = []; const propertiesByKey: PropertyMap = Object.create(null); const settersByKey: PropertyMap = Object.create(null); + const gettersByKey: PropertyMap = Object.create(null); const unmatchableProperties: ExpressionEntity[] = []; const unmatchableSetters: ExpressionEntity[] = []; + const unmatchableGetters: ExpressionEntity[] = []; for (let index = properties.length - 1; index >= 0; index--) { const { key, kind, property } = properties[index]; allProperties.push(property); @@ -154,15 +179,24 @@ function getPropertyMaps( } else { if (typeof key !== 'string') { unmatchableProperties.push(property); + if (kind === 'get') { + unmatchableGetters.push(property); + } } else if (!propertiesByKey[key]) { propertiesByKey[key] = [property, ...unmatchableProperties]; + gettersByKey[key] = [...unmatchableGetters]; + if (kind === 'get') { + gettersByKey[key].push(property); + } } } } return { allProperties, + gettersByKey, propertiesByKey, settersByKey, + unmatchableGetters, unmatchableProperties, unmatchableSetters }; diff --git a/src/ast/values.ts b/src/ast/values.ts index 0d7ea2ca50f..27e6850be76 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -290,6 +290,7 @@ const returnsString: RawMemberDescription = { } }; +// TODO Lukas instead use an object entity export class UnknownObjectExpression extends ValueBase { included = false; From 132fb94579ea5b1d28d373ac6af6488392ef6d71 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 21 Apr 2021 07:44:17 +0200 Subject: [PATCH 15/50] merge ObjectPathHandler into ObjectEntity --- src/ast/nodes/ObjectExpression.ts | 3 +- src/ast/nodes/shared/ClassNode.ts | 3 +- src/ast/nodes/shared/ObjectEntity.ts | 211 +++++++++++++++++++++++---- src/ast/utils/ObjectPathHandler.ts | 203 -------------------------- 4 files changed, 181 insertions(+), 239 deletions(-) delete mode 100644 src/ast/utils/ObjectPathHandler.ts diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index c3b6af27b91..f454752a22a 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,7 +4,6 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { ObjectProperty } from '../utils/ObjectPathHandler'; import { EMPTY_PATH, ObjectPath, @@ -19,7 +18,7 @@ import * as NodeType from './NodeType'; import Property from './Property'; import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; -import { ObjectEntity } from './shared/ObjectEntity'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import SpreadElement from './SpreadElement'; export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index ab16a4dbb8d..b6a806e88d5 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -3,7 +3,6 @@ import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { ObjectProperty } from '../../utils/ObjectPathHandler'; import { EMPTY_PATH, ObjectPath, @@ -18,7 +17,7 @@ import Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; import { ExpressionEntity } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; -import { ObjectEntity } from './ObjectEntity'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index de618cf5510..abcc2a7d299 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,8 +1,13 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { getObjectPathHandler, ObjectProperty } from '../../utils/ObjectPathHandler'; -import { ObjectPath, ObjectPathKey, PathTracker } from '../../utils/PathTracker'; +import { + ObjectPath, + ObjectPathKey, + PathTracker, + UnknownKey, + UNKNOWN_PATH +} from '../../utils/PathTracker'; import { getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, @@ -15,29 +20,78 @@ import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; import { ExpressionNode } from './Node'; +export interface ObjectProperty { + key: ObjectPathKey; + kind: 'init' | 'set' | 'get'; + property: ExpressionEntity; +} + +type PropertyMap = Record; + export class ObjectEntity implements ExpressionEntity { - deoptimizeAllProperties: () => void; - deoptimizePath: (path: ObjectPath) => void; - hasEffectsWhenAccessedAtPath: (path: ObjectPath, context: HasEffectsContext) => boolean; - hasEffectsWhenAssignedAtPath: (path: ObjectPath, context: HasEffectsContext) => boolean; included = false; - private getMemberExpression: (key: ObjectPathKey) => ExpressionEntity | null; - private getMemberExpressionAndTrackDeopt: ( - key: ObjectPathKey, - origin: DeoptimizableEntity - ) => ExpressionEntity | null; + private readonly allProperties: ExpressionEntity[] = []; + private readonly deoptimizedPaths = new Set(); + private readonly expressionsToBeDeoptimizedByKey: Record< + string, + DeoptimizableEntity[] + > = Object.create(null); + private readonly gettersByKey: PropertyMap = Object.create(null); + private hasUnknownDeoptimizedProperty = false; + private readonly propertiesByKey: PropertyMap = Object.create(null); + private readonly settersByKey: PropertyMap = Object.create(null); + private readonly unmatchableGetters: ExpressionEntity[] = []; + private readonly unmatchableProperties: ExpressionEntity[] = []; + private readonly unmatchableSetters: ExpressionEntity[] = []; - // TODO Lukas make this a proper class that we can extend? constructor(properties: ObjectProperty[]) { - ({ - deoptimizeAllProperties: this.deoptimizeAllProperties, - deoptimizePath: this.deoptimizePath, - getMemberExpression: this.getMemberExpression, - getMemberExpressionAndTrackDeopt: this.getMemberExpressionAndTrackDeopt, - hasEffectsWhenAccessedAtPath: this.hasEffectsWhenAccessedAtPath, - hasEffectsWhenAssignedAtPath: this.hasEffectsWhenAssignedAtPath - } = getObjectPathHandler(properties)); + this.buildPropertyMaps(properties); + } + + deoptimizeAllProperties() { + if (this.hasUnknownDeoptimizedProperty) return; + this.hasUnknownDeoptimizedProperty = true; + for (const property of this.allProperties) { + property.deoptimizePath(UNKNOWN_PATH); + } + for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + + deoptimizePath(path: ObjectPath) { + if (this.hasUnknownDeoptimizedProperty) return; + const key = path[0]; + if (path.length === 1) { + if (typeof key !== 'string') { + this.deoptimizeAllProperties(); + return; + } + if (!this.deoptimizedPaths.has(key)) { + this.deoptimizedPaths.add(key); + + // we only deoptimizeCache exact matches as in all other cases, + // we do not return a literal value or return expression + const expressionsToBeDeoptimized = this.expressionsToBeDeoptimizedByKey[key]; + if (expressionsToBeDeoptimized) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + } + const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + + for (const property of typeof key === 'string' + ? (this.propertiesByKey[key] || this.unmatchableProperties).concat( + this.settersByKey[key] || this.unmatchableSetters + ) + : this.allProperties) { + property.deoptimizePath(subPath); + } } getLiteralValueAtPath( @@ -49,10 +103,7 @@ export class ObjectEntity implements ExpressionEntity { return UnknownValue; } const key = path[0]; - const expressionAtPath = this.getMemberExpressionAndTrackDeopt( - key, - origin - ); + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); if (expressionAtPath) { return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); } @@ -71,10 +122,7 @@ export class ObjectEntity implements ExpressionEntity { return UNKNOWN_EXPRESSION; } const key = path[0]; - const expressionAtPath = this.getMemberExpressionAndTrackDeopt( - key, - origin - ); + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); if (expressionAtPath) { return expressionAtPath.getReturnExpressionWhenCalledAtPath( path.slice(1), @@ -88,6 +136,38 @@ export class ObjectEntity implements ExpressionEntity { return UNKNOWN_EXPRESSION; } + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + const expressionAtPath = this.getMemberExpression(key); + return !expressionAtPath || expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); + } + + if (typeof key !== 'string') return true; + + const properties = this.gettersByKey[key] || this.unmatchableGetters; + for (const property of properties) { + if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + return false; + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + const [key, ...subPath] = path; + if (path.length > 1) { + const expressionAtPath = this.getMemberExpression(key); + return !expressionAtPath || expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + } + + if (typeof key !== 'string') return true; + + const properties = this.settersByKey[key] || this.unmatchableSetters; + for (const property of properties) { + if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + return false; + } + hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, @@ -123,10 +203,7 @@ export class ObjectEntity implements ExpressionEntity { return false; } const key = path[0]; - const expressionAtPath = this.getMemberExpressionAndTrackDeopt( - key, - origin - ); + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); if (expressionAtPath) { return expressionAtPath.mayModifyThisWhenCalledAtPath( path.slice(1), @@ -136,4 +213,74 @@ export class ObjectEntity implements ExpressionEntity { } return false; } + + private buildPropertyMaps(properties: ObjectProperty[]): void { + const { + allProperties, + propertiesByKey, + settersByKey, + gettersByKey, + unmatchableProperties, + unmatchableGetters, + unmatchableSetters + } = this; + for (let index = properties.length - 1; index >= 0; index--) { + const { key, kind, property } = properties[index]; + allProperties.push(property); + if (kind === 'set') { + if (typeof key !== 'string') { + unmatchableSetters.push(property); + } else if (!settersByKey[key]) { + settersByKey[key] = [property, ...unmatchableSetters]; + } + } else { + if (typeof key !== 'string') { + unmatchableProperties.push(property); + if (kind === 'get') { + unmatchableGetters.push(property); + } + } else if (!propertiesByKey[key]) { + propertiesByKey[key] = [property, ...unmatchableProperties]; + gettersByKey[key] = [...unmatchableGetters]; + if (kind === 'get') { + gettersByKey[key].push(property); + } + } + } + } + } + + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { + if ( + this.hasUnknownDeoptimizedProperty || + typeof key !== 'string' || + this.deoptimizedPaths.has(key) + ) { + return UNKNOWN_EXPRESSION; + } + const properties = this.propertiesByKey[key]; + if (properties?.length === 1) { + return properties[0]; + } + if (properties || this.unmatchableProperties.length > 0) { + return UNKNOWN_EXPRESSION; + } + return null; + } + + private getMemberExpressionAndTrackDeopt( + key: ObjectPathKey, + origin: DeoptimizableEntity + ): ExpressionEntity | null { + if (key === UnknownKey) { + return UNKNOWN_EXPRESSION; + } + const expression = this.getMemberExpression(key); + if (expression !== UNKNOWN_EXPRESSION) { + const expressionsToBeDeoptimized = (this.expressionsToBeDeoptimizedByKey[key] = + this.expressionsToBeDeoptimizedByKey[key] || []); + expressionsToBeDeoptimized.push(origin); + } + return expression; + } } diff --git a/src/ast/utils/ObjectPathHandler.ts b/src/ast/utils/ObjectPathHandler.ts deleted file mode 100644 index eae5b0d22ce..00000000000 --- a/src/ast/utils/ObjectPathHandler.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext } from '../ExecutionContext'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../values'; -import { ObjectPath, ObjectPathKey, UnknownKey, UNKNOWN_PATH } from './PathTracker'; - -export interface ObjectProperty { - key: ObjectPathKey; - kind: 'init' | 'set' | 'get'; - property: ExpressionEntity; -} - -type PropertyMap = Record; - -export type ObjectPathHandler = ReturnType; - -export function getObjectPathHandler(properties: ObjectProperty[]) { - const { - allProperties, - propertiesByKey, - gettersByKey, - settersByKey, - unmatchableGetters, - unmatchableProperties, - unmatchableSetters - } = getPropertyMaps(properties); - - let hasUnknownDeoptimizedProperty = false; - const deoptimizedPaths = new Set(); - const expressionsToBeDeoptimizedByKey: Record = Object.create( - null - ); - - // TODO Lukas it would be really interesting to know if we have any getters here? - // -> Add a third table of getters here! - function getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { - if (hasUnknownDeoptimizedProperty || typeof key !== 'string' || deoptimizedPaths.has(key)) { - return UNKNOWN_EXPRESSION; - } - if (propertiesByKey[key]?.length === 1) { - return propertiesByKey[key][0]; - } - if (propertiesByKey[key] || unmatchableProperties.length > 0) { - return UNKNOWN_EXPRESSION; - } - return null; - } - - function getMemberExpressionAndTrackDeopt( - key: ObjectPathKey, - origin: DeoptimizableEntity - ): ExpressionEntity | null { - if (key === UnknownKey) { - return UNKNOWN_EXPRESSION; - } - const expression = getMemberExpression(key); - if (expression !== UNKNOWN_EXPRESSION) { - const expressionsToBeDeoptimized = (expressionsToBeDeoptimizedByKey[key] = - expressionsToBeDeoptimizedByKey[key] || []); - expressionsToBeDeoptimized.push(origin); - } - return expression; - } - - function hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const [key, ...subPath] = path; - if (path.length > 1) { - const expressionAtPath = getMemberExpression(key); - return !expressionAtPath || expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); - } - - if (typeof key !== 'string') return true; - - const properties = gettersByKey[key] || unmatchableGetters; - for (const property of properties) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - return false; - } - - function hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - const [key, ...subPath] = path; - if (path.length > 1) { - const expressionAtPath = getMemberExpression(key); - return !expressionAtPath || expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); - } - - if (typeof key !== 'string') return true; - - const properties = settersByKey[key] || unmatchableSetters; - for (const property of properties) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; - } - return false; - } - - function deoptimizePath(path: ObjectPath) { - if (hasUnknownDeoptimizedProperty) return; - const key = path[0]; - if (path.length === 1) { - if (typeof key !== 'string') { - deoptimizeAllProperties(); - return; - } - if (!deoptimizedPaths.has(key)) { - deoptimizedPaths.add(key); - - // we only deoptimizeCache exact matches as in all other cases, - // we do not return a literal value or return expression - const expressionsToBeDeoptimized = expressionsToBeDeoptimizedByKey[key]; - if (expressionsToBeDeoptimized) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - } - const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); - - for (const property of typeof key === 'string' - ? (propertiesByKey[key] || unmatchableProperties).concat( - settersByKey[key] || unmatchableSetters - ) - : allProperties) { - property.deoptimizePath(subPath); - } - } - - function deoptimizeAllProperties() { - if (hasUnknownDeoptimizedProperty) return; - hasUnknownDeoptimizedProperty = true; - for (const property of allProperties) { - property.deoptimizePath(UNKNOWN_PATH); - } - for (const expressionsToBeDeoptimized of Object.values(expressionsToBeDeoptimizedByKey)) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } - } - } - - return { - deoptimizeAllProperties, - deoptimizePath, - getMemberExpression, - getMemberExpressionAndTrackDeopt, - hasEffectsWhenAccessedAtPath, - hasEffectsWhenAssignedAtPath - }; -} - -function getPropertyMaps( - properties: ObjectProperty[] -): { - allProperties: ExpressionEntity[]; - gettersByKey: Record; - propertiesByKey: Record; - settersByKey: Record; - unmatchableGetters: ExpressionEntity[]; - unmatchableProperties: ExpressionEntity[]; - unmatchableSetters: ExpressionEntity[]; -} { - const allProperties = []; - const propertiesByKey: PropertyMap = Object.create(null); - const settersByKey: PropertyMap = Object.create(null); - const gettersByKey: PropertyMap = Object.create(null); - const unmatchableProperties: ExpressionEntity[] = []; - const unmatchableSetters: ExpressionEntity[] = []; - const unmatchableGetters: ExpressionEntity[] = []; - for (let index = properties.length - 1; index >= 0; index--) { - const { key, kind, property } = properties[index]; - allProperties.push(property); - if (kind === 'set') { - if (typeof key !== 'string') { - unmatchableSetters.push(property); - } else if (!settersByKey[key]) { - settersByKey[key] = [property, ...unmatchableSetters]; - } - } else { - if (typeof key !== 'string') { - unmatchableProperties.push(property); - if (kind === 'get') { - unmatchableGetters.push(property); - } - } else if (!propertiesByKey[key]) { - propertiesByKey[key] = [property, ...unmatchableProperties]; - gettersByKey[key] = [...unmatchableGetters]; - if (kind === 'get') { - gettersByKey[key].push(property); - } - } - } - } - return { - allProperties, - gettersByKey, - propertiesByKey, - settersByKey, - unmatchableGetters, - unmatchableProperties, - unmatchableSetters - }; -} From f09cd5bf6e05781fa7f2608e3d3b47cd0373b19f Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 21 Apr 2021 15:21:20 +0200 Subject: [PATCH 16/50] Slightly refactor default values --- src/ast/nodes/shared/ClassNode.ts | 6 ++++ src/ast/values.ts | 51 +++++++++++-------------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index b6a806e88d5..4dbe00bea85 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -19,6 +19,12 @@ import { ExpressionEntity } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +// TODO Lukas +// * Create an object entity for the object prototype +// * Introduce a prototype expression and use the entity +// * Use the object prototype also for unknown expressions +// * __proto__ assignment handling might be possible solely via the object prototype? But it would need to deoptimize the entire prototype chain: Bad. Better we always replace the prototype with "unknown" on assigment +// * __proto__: foo handling however is an ObjectExpression feature export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; id!: Identifier | null; diff --git a/src/ast/values.ts b/src/ast/values.ts index 27e6850be76..fec0c21781c 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -31,7 +31,7 @@ function assembleMemberDescriptions( export const UnknownValue = Symbol('Unknown Value'); export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; -abstract class ValueBase implements ExpressionEntity { +class ValueBase implements ExpressionEntity { included = true; deoptimizePath() {} @@ -65,24 +65,18 @@ abstract class ValueBase implements ExpressionEntity { include() {} - includeCallArguments(_context: InclusionContext, _args: (ExpressionNode | SpreadElement)[]) {} + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { + for (const arg of args) { + arg.include(context, false); + } + } mayModifyThisWhenCalledAtPath() { return true; } } -function includeAll(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } -} - -export const UNKNOWN_EXPRESSION: ExpressionEntity = new class extends ValueBase { - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } -}; +export const UNKNOWN_EXPRESSION: ExpressionEntity = new class UnknownExpression extends ValueBase {}; -export const UNDEFINED_EXPRESSION: ExpressionEntity = new class extends ValueBase { +export const UNDEFINED_EXPRESSION: ExpressionEntity = new class UndefinedExpression extends ValueBase { getLiteralValueAtPath() { return undefined; } @@ -135,10 +129,6 @@ export class UnknownArrayExpression extends ValueBase { include() { this.included = true; } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } } const returnsArray: RawMemberDescription = { @@ -174,16 +164,18 @@ const callsArgMutatesSelfReturnsArray: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class extends ValueBase { +const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends ValueBase { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); } return UNKNOWN_EXPRESSION; } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1 } + hasEffectsWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { const subPath = path[0]; @@ -191,9 +183,6 @@ const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class extends ValueBase { } return true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } }; const returnsBoolean: RawMemberDescription = { @@ -213,14 +202,16 @@ const callsArgReturnsBoolean: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class extends ValueBase { +const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends ValueBase { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); } return UNKNOWN_EXPRESSION; } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } + hasEffectsWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { const subPath = path[0]; @@ -228,9 +219,6 @@ const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class extends ValueBase { } return true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } }; const returnsNumber: RawMemberDescription = { @@ -258,14 +246,16 @@ const callsArgReturnsNumber: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class extends ValueBase { +const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends ValueBase { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); } return UNKNOWN_EXPRESSION; } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1 } + hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, @@ -276,9 +266,6 @@ const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class extends ValueBase { } return true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } }; const returnsString: RawMemberDescription = { @@ -323,10 +310,6 @@ export class UnknownObjectExpression extends ValueBase { include() { this.included = true; } - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - includeAll(context, args); - } } export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ From 10ee697a9ee9a07971474246bc13459da0edb4cf Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 21 Apr 2021 15:52:50 +0200 Subject: [PATCH 17/50] Separate unknown nodes from other Nodes to avoid future circular dependencies --- src/Chunk.ts | 2 +- src/ast/nodes/ArrayExpression.ts | 2 +- src/ast/nodes/ArrayPattern.ts | 2 +- src/ast/nodes/ArrowFunctionExpression.ts | 2 +- src/ast/nodes/BinaryExpression.ts | 2 +- src/ast/nodes/BlockStatement.ts | 2 +- src/ast/nodes/CallExpression.ts | 2 +- src/ast/nodes/CatchClause.ts | 2 +- src/ast/nodes/ConditionalExpression.ts | 2 +- src/ast/nodes/Identifier.ts | 2 +- src/ast/nodes/IfStatement.ts | 2 +- src/ast/nodes/Literal.ts | 4 +- src/ast/nodes/LogicalExpression.ts | 2 +- src/ast/nodes/MemberExpression.ts | 2 +- src/ast/nodes/MethodDefinition.ts | 6 +-- src/ast/nodes/ObjectExpression.ts | 2 +- src/ast/nodes/Property.ts | 2 +- src/ast/nodes/PropertyDefinition.ts | 2 +- src/ast/nodes/RestElement.ts | 2 +- src/ast/nodes/ReturnStatement.ts | 2 +- src/ast/nodes/SequenceExpression.ts | 2 +- src/ast/nodes/TemplateLiteral.ts | 2 +- src/ast/nodes/UnaryExpression.ts | 2 +- src/ast/nodes/VariableDeclarator.ts | 2 +- src/ast/nodes/shared/ClassNode.ts | 2 +- src/ast/nodes/shared/Expression.ts | 2 +- src/ast/nodes/shared/FunctionNode.ts | 3 +- src/ast/nodes/shared/MultiExpression.ts | 2 +- src/ast/nodes/shared/Node.ts | 2 +- src/ast/nodes/shared/ObjectEntity.ts | 6 +-- src/ast/scopes/BlockScope.ts | 2 +- src/ast/scopes/ModuleScope.ts | 2 +- src/ast/scopes/ParameterScope.ts | 2 +- src/ast/scopes/ReturnValueScope.ts | 2 +- src/ast/scopes/Scope.ts | 2 +- src/ast/unknownValues.ts | 61 +++++++++++++++++++++ src/ast/values.ts | 68 +++--------------------- src/ast/variables/ArgumentsVariable.ts | 2 +- src/ast/variables/LocalVariable.ts | 2 +- src/ast/variables/ThisVariable.ts | 2 +- src/ast/variables/UndefinedVariable.ts | 2 +- src/ast/variables/Variable.ts | 2 +- 42 files changed, 112 insertions(+), 108 deletions(-) create mode 100644 src/ast/unknownValues.ts diff --git a/src/Chunk.ts b/src/Chunk.ts index 0d79ffb1811..9f583ea4a3e 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -3,7 +3,7 @@ import { relative } from '../browser/path'; import ExportDefaultDeclaration from './ast/nodes/ExportDefaultDeclaration'; import FunctionDeclaration from './ast/nodes/FunctionDeclaration'; import ChildScope from './ast/scopes/ChildScope'; -import { UNDEFINED_EXPRESSION } from './ast/values'; +import { UNDEFINED_EXPRESSION } from './ast/unknownValues'; import ExportDefaultVariable from './ast/variables/ExportDefaultVariable'; import ExportShimVariable from './ast/variables/ExportShimVariable'; import LocalVariable from './ast/variables/LocalVariable'; diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index f37b81dbbc5..dcc9ffdf403 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,11 +1,11 @@ import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import { arrayMembers, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import { ExpressionNode, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index e73f591fd7b..ed21bf3bd28 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -1,6 +1,6 @@ import { HasEffectsContext } from '../ExecutionContext'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 52c08376857..1664fd73a9b 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -2,8 +2,8 @@ import { CallOptions } from '../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import ReturnValueScope from '../scopes/ReturnValueScope'; import Scope from '../scopes/Scope'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index eb85497e630..3a26ebaba7c 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -1,12 +1,12 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExpressionStatement from './ExpressionStatement'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index f60c30ee830..c9c4166f950 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -4,7 +4,7 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import ChildScope from '../scopes/ChildScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import ExpressionStatement from './ExpressionStatement'; import * as NodeType from './NodeType'; import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node'; diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index da9362598a1..daab90dbca0 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,6 +9,7 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -16,7 +17,6 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/CatchClause.ts b/src/ast/nodes/CatchClause.ts index c7885e8ab80..41acfb00cee 100644 --- a/src/ast/nodes/CatchClause.ts +++ b/src/ast/nodes/CatchClause.ts @@ -1,6 +1,6 @@ import CatchScope from '../scopes/CatchScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import BlockStatement from './BlockStatement'; import * as NodeType from './NodeType'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 2b1dfea88a3..46b5bcb1bc8 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -18,7 +19,6 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 9c5b787fa73..1a4995d7ee6 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -7,8 +7,8 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import FunctionScope from '../scopes/FunctionScope'; +import { LiteralValueOrUnknown } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 23667804428..1e2fa374b70 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -4,8 +4,8 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import TrackingScope from '../scopes/TrackingScope'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index fc55458ea11..964d05e9c43 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -1,15 +1,13 @@ import MagicString from 'magic-string'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; import { getLiteralMembersForValue, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - LiteralValueOrUnknown, MemberDescription, - UnknownValue, - UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index e19ba265c9e..b6d89366d91 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -18,7 +19,6 @@ import { SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index aee336f1e2b..1acf0d513de 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -6,6 +6,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -15,7 +16,6 @@ import { UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; import Variable from '../variables/Variable'; diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index 546898ba8d6..a13497d6145 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -1,8 +1,8 @@ import { CallOptions, NO_ARGS } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../values'; import FunctionExpression from './FunctionExpression'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; @@ -83,9 +83,9 @@ export default class MethodDefinition extends NodeBase { if (this.accessedValue === null) { if (this.kind === 'get') { this.accessedValue = UNKNOWN_EXPRESSION; - this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH); + return this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH); } else { - this.accessedValue = this.value; + return this.accessedValue = this.value; } } return this.accessedValue; diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index f454752a22a..9fe4f0fa08b 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,6 +4,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -11,7 +12,6 @@ import { SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index ee8d4af68cc..f839b30819c 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -4,6 +4,7 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { CallOptions, NO_ARGS } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -11,7 +12,6 @@ import { SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 80964fb743b..eac3ea58d48 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,8 +1,8 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; import { ExpressionEntity } from './shared/Expression'; diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 1ea98754e47..1b06d71be30 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,6 +1,6 @@ import { HasEffectsContext } from '../ExecutionContext'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index 87bec1a78c2..a5bed281411 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -5,7 +5,7 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 13930c336f7..caf8a694176 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -10,8 +10,8 @@ import { treeshakeNode } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../values'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/TemplateLiteral.ts b/src/ast/nodes/TemplateLiteral.ts index fbc02c708ee..198345f2785 100644 --- a/src/ast/nodes/TemplateLiteral.ts +++ b/src/ast/nodes/TemplateLiteral.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import * as NodeType from './NodeType'; import { ExpressionNode, NodeBase } from './shared/Node'; import TemplateElement from './TemplateElement'; diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index b80b70474ea..df261153451 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,7 +1,7 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../values'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/VariableDeclarator.ts b/src/ast/nodes/VariableDeclarator.ts index 40cd81a57db..333ebbfd860 100644 --- a/src/ast/nodes/VariableDeclarator.ts +++ b/src/ast/nodes/VariableDeclarator.ts @@ -7,8 +7,8 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { UNDEFINED_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; -import { UNDEFINED_EXPRESSION } from '../values'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 4dbe00bea85..5d01b9b9458 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -3,6 +3,7 @@ import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; +import { LiteralValueOrUnknown, UnknownValue } from '../../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -10,7 +11,6 @@ import { SHARED_RECURSION_TRACKER, UnknownKey } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../../values'; import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import Literal from '../Literal'; diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index e579388baca..48614f18c30 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,8 +2,8 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown } from '../../values'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 65e81bed072..0daab867b23 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,8 +1,9 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import FunctionScope from '../../scopes/FunctionScope'; +import { UNKNOWN_EXPRESSION } from '../../unknownValues'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { UnknownObjectExpression, UNKNOWN_EXPRESSION } from '../../values'; +import { UnknownObjectExpression } from '../../values'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index 2be9e81c6b3..0a6943f9d7d 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -1,8 +1,8 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue } from '../../values'; import { ExpressionEntity } from './Expression'; import { IncludeChildren } from './Node'; diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 22f5f7fb0ba..a3792a9e2ab 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -13,8 +13,8 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import ChildScope from '../../scopes/ChildScope'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../values'; import Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; import SpreadElement from '../SpreadElement'; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index abcc2a7d299..907e462b1b0 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,6 +1,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; import { ObjectPath, ObjectPathKey, @@ -11,10 +12,7 @@ import { import { getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - LiteralValueOrUnknown, - objectMembers, - UnknownValue, - UNKNOWN_EXPRESSION + objectMembers } from '../../values'; import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; diff --git a/src/ast/scopes/BlockScope.ts b/src/ast/scopes/BlockScope.ts index b72bcc3a19e..847263dfb2d 100644 --- a/src/ast/scopes/BlockScope.ts +++ b/src/ast/scopes/BlockScope.ts @@ -1,7 +1,7 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; diff --git a/src/ast/scopes/ModuleScope.ts b/src/ast/scopes/ModuleScope.ts index 372839a7dd0..816d77e1670 100644 --- a/src/ast/scopes/ModuleScope.ts +++ b/src/ast/scopes/ModuleScope.ts @@ -1,7 +1,7 @@ import { AstContext } from '../../Module'; import { InternalModuleFormat } from '../../rollup/types'; import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; -import { UNDEFINED_EXPRESSION } from '../values'; +import { UNDEFINED_EXPRESSION } from '../unknownValues'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index 8aafd8d4e09..d24075f8781 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -3,7 +3,7 @@ import { InclusionContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { UNKNOWN_EXPRESSION } from '../values'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; import Scope from './Scope'; diff --git a/src/ast/scopes/ReturnValueScope.ts b/src/ast/scopes/ReturnValueScope.ts index 8647cd299ba..f276a5978b4 100644 --- a/src/ast/scopes/ReturnValueScope.ts +++ b/src/ast/scopes/ReturnValueScope.ts @@ -1,6 +1,6 @@ import { ExpressionEntity } from '../nodes/shared/Expression'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { UNKNOWN_PATH } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import ParameterScope from './ParameterScope'; export default class ReturnValueScope extends ParameterScope { diff --git a/src/ast/scopes/Scope.ts b/src/ast/scopes/Scope.ts index 32a46d31719..4f99aa86f42 100644 --- a/src/ast/scopes/Scope.ts +++ b/src/ast/scopes/Scope.ts @@ -1,7 +1,7 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNDEFINED_EXPRESSION } from '../values'; +import { UNDEFINED_EXPRESSION } from '../unknownValues'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import ChildScope from './ChildScope'; diff --git a/src/ast/unknownValues.ts b/src/ast/unknownValues.ts new file mode 100644 index 00000000000..d4a89e1d167 --- /dev/null +++ b/src/ast/unknownValues.ts @@ -0,0 +1,61 @@ +import { CallOptions } from './CallOptions'; +import { HasEffectsContext, InclusionContext } from './ExecutionContext'; +import { LiteralValue } from './nodes/Literal'; +import { ExpressionEntity } from './nodes/shared/Expression'; +import { ExpressionNode } from './nodes/shared/Node'; +import SpreadElement from './nodes/SpreadElement'; +import { ObjectPath } from './utils/PathTracker'; + +export const UnknownValue = Symbol('Unknown Value'); +export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; + +export class UnknownExpression implements ExpressionEntity { + included = true; + + deoptimizePath() {} + + getLiteralValueAtPath(): LiteralValueOrUnknown { + return UnknownValue; + } + + getReturnExpressionWhenCalledAtPath(_path: ObjectPath) { + return UNKNOWN_EXPRESSION; + } + + hasEffectsWhenAccessedAtPath( + path: ObjectPath, + _context: HasEffectsContext + ) { + return path.length > 0 + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath) { + return path.length > 0 + } + + hasEffectsWhenCalledAtPath( + _path: ObjectPath, + _callOptions: CallOptions, + _context: HasEffectsContext + ) { + return true; + } + + include() {} + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { + for (const arg of args) { + arg.include(context, false); + } + } + + mayModifyThisWhenCalledAtPath() { return true; } +} + +export const UNKNOWN_EXPRESSION: ExpressionEntity = new UnknownExpression(); + +export const UNDEFINED_EXPRESSION: ExpressionEntity = new class UndefinedExpression extends UnknownExpression { + getLiteralValueAtPath() { + return undefined; + } +}; diff --git a/src/ast/values.ts b/src/ast/values.ts index fec0c21781c..d8f5644eff5 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,9 +1,8 @@ import { CallOptions, NO_ARGS } from './CallOptions'; -import { HasEffectsContext, InclusionContext } from './ExecutionContext'; +import { HasEffectsContext } from './ExecutionContext'; import { LiteralValue } from './nodes/Literal'; import { ExpressionEntity } from './nodes/shared/Expression'; -import { ExpressionNode } from './nodes/shared/Node'; -import SpreadElement from './nodes/SpreadElement'; +import { UNKNOWN_EXPRESSION, UnknownExpression } from './unknownValues'; import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { @@ -28,59 +27,6 @@ function assembleMemberDescriptions( return Object.create(inheritedDescriptions, memberDescriptions); } -export const UnknownValue = Symbol('Unknown Value'); -export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; - -class ValueBase implements ExpressionEntity { - included = true; - - deoptimizePath() {} - - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath(_path: ObjectPath) { - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath( - path: ObjectPath, - _context: HasEffectsContext - ) { - return path.length > 0 - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 0 - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - - include() {} - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } - } - - mayModifyThisWhenCalledAtPath() { return true; } -} - -export const UNKNOWN_EXPRESSION: ExpressionEntity = new class UnknownExpression extends ValueBase {}; - -export const UNDEFINED_EXPRESSION: ExpressionEntity = new class UndefinedExpression extends ValueBase { - getLiteralValueAtPath() { - return undefined; - } -}; const returnsUnknown: RawMemberDescription = { value: { @@ -97,7 +43,7 @@ const callsArgReturnsUnknown: RawMemberDescription = { value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: [0], mutatesSelf: false } }; -export class UnknownArrayExpression extends ValueBase { +export class UnknownArrayExpression extends UnknownExpression { included = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { @@ -164,7 +110,7 @@ const callsArgMutatesSelfReturnsArray: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends ValueBase { +const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends UnknownExpression { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); @@ -202,7 +148,7 @@ const callsArgReturnsBoolean: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends ValueBase { +const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends UnknownExpression { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); @@ -246,7 +192,7 @@ const callsArgReturnsNumber: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends ValueBase { +const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends UnknownExpression { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); @@ -278,7 +224,7 @@ const returnsString: RawMemberDescription = { }; // TODO Lukas instead use an object entity -export class UnknownObjectExpression extends ValueBase { +export class UnknownObjectExpression extends UnknownExpression { included = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { diff --git a/src/ast/variables/ArgumentsVariable.ts b/src/ast/variables/ArgumentsVariable.ts index b67ff2f0de4..234f389b0cc 100644 --- a/src/ast/variables/ArgumentsVariable.ts +++ b/src/ast/variables/ArgumentsVariable.ts @@ -1,6 +1,6 @@ import { AstContext } from '../../Module'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; -import { UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from './LocalVariable'; export default class ArgumentsVariable extends LocalVariable { diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index ee0f5030043..a1ac4c39f8a 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -8,8 +8,8 @@ import * as NodeType from '../nodes/NodeType'; import { ExpressionEntity } from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import Variable from './Variable'; // To avoid infinite recursions diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index b0fbe27e39a..839c635f4f1 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -2,8 +2,8 @@ import { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; import { ExpressionEntity } from '../nodes/shared/Expression'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; import LocalVariable from './LocalVariable'; export default class ThisVariable extends LocalVariable { diff --git a/src/ast/variables/UndefinedVariable.ts b/src/ast/variables/UndefinedVariable.ts index 32e45285abd..2cf9eee9643 100644 --- a/src/ast/variables/UndefinedVariable.ts +++ b/src/ast/variables/UndefinedVariable.ts @@ -1,4 +1,4 @@ -import { LiteralValueOrUnknown } from '../values'; +import { LiteralValueOrUnknown } from '../unknownValues'; import Variable from './Variable'; export default class UndefinedVariable extends Variable { diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 9de050fb0c2..3386aa1777d 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -8,8 +8,8 @@ import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../values'; export default class Variable implements ExpressionEntity { alwaysRendered = false; From 035011097c0ef0275d77cfc2377f6021057a2ce6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 22 Apr 2021 07:24:56 +0200 Subject: [PATCH 18/50] Introduce new prototype tracking --- src/ast/Entity.ts | 2 +- src/ast/nodes/CallExpression.ts | 2 +- src/ast/nodes/MemberExpression.ts | 2 +- src/ast/nodes/MethodDefinition.ts | 16 ++- src/ast/nodes/ObjectExpression.ts | 7 +- src/ast/nodes/shared/ClassNode.ts | 24 ++-- src/ast/nodes/shared/Expression.ts | 4 + src/ast/nodes/shared/MethodTypes.ts | 112 ++++++++++++++++++ src/ast/nodes/shared/MultiExpression.ts | 6 + src/ast/nodes/shared/Node.ts | 4 +- src/ast/nodes/shared/ObjectEntity.ts | 66 +++++++---- src/ast/nodes/shared/ObjectMember.ts | 77 ++++++++++++ src/ast/nodes/shared/ObjectPrototype.ts | 18 +++ src/ast/unknownValues.ts | 20 ++-- src/ast/values.ts | 10 +- src/ast/variables/Variable.ts | 2 + .../modify-class-prototype/_expected.js | 8 ++ .../samples/modify-class-prototype/main.js | 15 +++ .../_expected.js | 10 +- .../main.js | 15 ++- 20 files changed, 356 insertions(+), 64 deletions(-) create mode 100644 src/ast/nodes/shared/MethodTypes.ts create mode 100644 src/ast/nodes/shared/ObjectMember.ts create mode 100644 src/ast/nodes/shared/ObjectPrototype.ts diff --git a/src/ast/Entity.ts b/src/ast/Entity.ts index 8ed7c1b179e..e08a3d9b33d 100644 --- a/src/ast/Entity.ts +++ b/src/ast/Entity.ts @@ -11,5 +11,5 @@ export interface WritableEntity extends Entity { * expression of this node is reassigned as well. */ deoptimizePath(path: ObjectPath): void; - hasEffectsWhenAssignedAtPath(path: ObjectPath, execution: HasEffectsContext): boolean; + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; } diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index daab90dbca0..ec853a8be5d 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,7 +9,7 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 1acf0d513de..ea34bbafa66 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -242,7 +242,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } return this.object.mayModifyThisWhenCalledAtPath( - [this.propertyKey as ObjectPathKey].concat(path), + [this.propertyKey!, ...path], recursionTracker, origin ); diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index a13497d6145..75a025fcaea 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -55,12 +55,22 @@ export default class MethodDefinition extends NodeBase { if (this.kind === 'get') { return ( this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - (path.length > 0 && this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context)) + this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context) ); } return this.value.hasEffectsWhenAccessedAtPath(path); } + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'get') { + return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); + } + if (this.kind === 'set') { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); + } + return this.value.hasEffectsWhenAccessedAtPath(path); + } + hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, @@ -83,9 +93,9 @@ export default class MethodDefinition extends NodeBase { if (this.accessedValue === null) { if (this.kind === 'get') { this.accessedValue = UNKNOWN_EXPRESSION; - return this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH); + return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH)); } else { - return this.accessedValue = this.value; + return (this.accessedValue = this.value); } } return this.accessedValue; diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 9fe4f0fa08b..c960509b9c7 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,7 +4,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -19,6 +19,7 @@ import Property from './Property'; import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; +import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; import SpreadElement from './SpreadElement'; export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { @@ -28,7 +29,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE private objectEntity: ObjectEntity | null = null; deoptimizeCache() { - this.getObjectEntity().deoptimizeAllProperties(); + this.getObjectEntity().deoptimizeObject(); } deoptimizePath(path: ObjectPath) { @@ -130,6 +131,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE } properties.push({ kind: property.kind, key, property }); } - return (this.objectEntity = new ObjectEntity(properties)); + return (this.objectEntity = new ObjectEntity(properties, OBJECT_PROTOTYPE)); } } diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 5d01b9b9458..0867d2446ed 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -18,11 +18,10 @@ import MethodDefinition from '../MethodDefinition'; import { ExpressionEntity } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { ObjectMember } from './ObjectMember'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; // TODO Lukas -// * Create an object entity for the object prototype -// * Introduce a prototype expression and use the entity -// * Use the object prototype also for unknown expressions // * __proto__ assignment handling might be possible solely via the object prototype? But it would need to deoptimize the entire prototype chain: Bad. Better we always replace the prototype with "unknown" on assigment // * __proto__: foo handling however is an ObjectExpression feature export default class ClassNode extends NodeBase implements DeoptimizableEntity { @@ -38,14 +37,13 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { } deoptimizeCache() { - this.getObjectEntity().deoptimizeAllProperties(); + this.getObjectEntity().deoptimizeObject(); } deoptimizePath(path: ObjectPath) { this.getObjectEntity().deoptimizePath(path); } - // TODO Lukas also check super class, prototype getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -54,7 +52,6 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { return this.getObjectEntity().getLiteralValueAtPath(path, recursionTracker, origin); } - // TODO Lukas also check super class getReturnExpressionWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -67,18 +64,14 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { ); } - // TODO Lukas also check super class hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - // TODO Lukas if there is no direct match and no effect, also check superclass return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); } - // TODO Lukas prototype hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } - // TODO Lukas also check super class hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, @@ -110,7 +103,6 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { this.classConstructor = null; } - // TODO Lukas also check super class mayModifyThisWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -153,8 +145,14 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { staticProperties.unshift({ key: 'prototype', kind: 'init', - property: new ObjectEntity(dynamicProperties) + property: new ObjectEntity( + dynamicProperties, + this.superClass ? new ObjectMember(this.superClass, 'prototype') : OBJECT_PROTOTYPE + ) }); - return (this.objectEntity = new ObjectEntity(staticProperties)); + return (this.objectEntity = new ObjectEntity( + staticProperties, + this.superClass || OBJECT_PROTOTYPE + )); } } diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 48614f18c30..c8612bcd5cd 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -10,6 +10,10 @@ import { ExpressionNode, IncludeChildren } from './Node'; export interface ExpressionEntity extends WritableEntity { included: boolean; + /** + * Assume existing properties have been mutated but do not assume properties have been added/overwritten. Important for mutations of objects with prototypes. + */ + deoptimizeProperties():void /** * If possible it returns a stringifyable literal value for this node that can be used * for inlining or comparing values. diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts new file mode 100644 index 00000000000..aa1a76927b9 --- /dev/null +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -0,0 +1,112 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; +import { EMPTY_PATH, ObjectPath } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_STRING } from '../../values'; +import SpreadElement from '../SpreadElement'; +import { ExpressionEntity } from './Expression'; +import { ExpressionNode } from './Node'; + +type MethodDescription = { + callsArgs: number[] | null; + mutatesSelf: boolean; +} & ( + | { + returns: { new (): ExpressionEntity }; + returnsPrimitive: null; + } + | { + returns: null; + returnsPrimitive: ExpressionEntity; + } +); + +class Method implements ExpressionEntity { + public included = true; + + constructor(private readonly description: MethodDescription) {} + + deoptimizePath(): void {} + + deoptimizeProperties(): void {} + + getLiteralValueAtPath(): LiteralValueOrUnknown { + return UnknownValue; + } + + getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity { + if (path.length > 0) { + return UNKNOWN_EXPRESSION; + } + return this.description.returnsPrimitive || new this.description.returns(); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { + return path.length > 1; + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath): boolean { + return path.length > 0; + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + if (path.length > 0) { + return true; + } + if (!this.description.callsArgs) { + return false; + } + for (const argIndex of this.description.callsArgs) { + if ( + callOptions.args[argIndex]?.hasEffectsWhenCalledAtPath( + EMPTY_PATH, + { + args: NO_ARGS, + withNew: false + }, + context + ) + ) { + return true; + } + } + return false; + } + + include(): void {} + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } + + mayModifyThisWhenCalledAtPath(path: ObjectPath): boolean { + return path.length === 0 && this.description.mutatesSelf; + } +} + +export const METHOD_RETURNS_BOOLEAN = new Method({ + callsArgs: null, + mutatesSelf: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN +}) + +export const METHOD_RETURNS_STRING = new Method({ + callsArgs: null, + mutatesSelf: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_STRING +}) + +export const METHOD_RETURNS_UNKNOWN = new Method({ + callsArgs: null, + mutatesSelf: false, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION +}) diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index 0a6943f9d7d..0c47a8c6a35 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -21,6 +21,12 @@ export class MultiExpression implements ExpressionEntity { } } + deoptimizeProperties() { + for (const expression of this.expressions) { + expression.deoptimizeProperties(); + } + } + getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index a3792a9e2ab..aa17475ee7c 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -13,7 +13,7 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import ChildScope from '../../scopes/ChildScope'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; import Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; @@ -151,6 +151,8 @@ export class NodeBase implements ExpressionNode { deoptimizePath(_path: ObjectPath) {} + deoptimizeProperties() {} + getLiteralValueAtPath( _path: ObjectPath, _recursionTracker: PathTracker, diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 907e462b1b0..4629809a946 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -9,11 +9,6 @@ import { UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, - objectMembers -} from '../../values'; import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; import { ExpressionNode } from './Node'; @@ -26,6 +21,7 @@ export interface ObjectProperty { type PropertyMap = Record; +// TODO Lukas add a way to directly inject only propertiesByKey and create allProperties lazily/not export class ObjectEntity implements ExpressionEntity { included = false; @@ -43,16 +39,14 @@ export class ObjectEntity implements ExpressionEntity { private readonly unmatchableProperties: ExpressionEntity[] = []; private readonly unmatchableSetters: ExpressionEntity[] = []; - constructor(properties: ObjectProperty[]) { + constructor(properties: ObjectProperty[], private prototypeExpression: ExpressionEntity | null) { this.buildPropertyMaps(properties); } - deoptimizeAllProperties() { + deoptimizeObject(): void { if (this.hasUnknownDeoptimizedProperty) return; this.hasUnknownDeoptimizedProperty = true; - for (const property of this.allProperties) { - property.deoptimizePath(UNKNOWN_PATH); - } + this.deoptimizeProperties(); for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -65,7 +59,7 @@ export class ObjectEntity implements ExpressionEntity { const key = path[0]; if (path.length === 1) { if (typeof key !== 'string') { - this.deoptimizeAllProperties(); + this.deoptimizeObject(); return; } if (!this.deoptimizedPaths.has(key)) { @@ -81,8 +75,9 @@ export class ObjectEntity implements ExpressionEntity { } } } - const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); + // TODO Lukas verify again why we need to handle the path.length === 1 case here + const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); for (const property of typeof key === 'string' ? (this.propertiesByKey[key] || this.unmatchableProperties).concat( this.settersByKey[key] || this.unmatchableSetters @@ -92,6 +87,13 @@ export class ObjectEntity implements ExpressionEntity { } } + deoptimizeProperties(): void { + for (const property of this.allProperties) { + property.deoptimizePath(UNKNOWN_PATH); + } + this.prototypeExpression?.deoptimizeProperties(); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -105,7 +107,10 @@ export class ObjectEntity implements ExpressionEntity { if (expressionAtPath) { return expressionAtPath.getLiteralValueAtPath(path.slice(1), recursionTracker, origin); } - if (path.length === 1 && !objectMembers[key as string]) { + if (this.prototypeExpression) { + return this.prototypeExpression.getLiteralValueAtPath(path, recursionTracker, origin); + } + if (path.length === 1) { return undefined; } return UnknownValue; @@ -128,8 +133,12 @@ export class ObjectEntity implements ExpressionEntity { origin ); } - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(objectMembers, key); + if (this.prototypeExpression) { + return this.prototypeExpression.getReturnExpressionWhenCalledAtPath( + path, + recursionTracker, + origin + ); } return UNKNOWN_EXPRESSION; } @@ -137,16 +146,24 @@ export class ObjectEntity implements ExpressionEntity { hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const [key, ...subPath] = path; if (path.length > 1) { + // TODO Lukas we can look at the prototype as well, but only if the property is known? const expressionAtPath = this.getMemberExpression(key); - return !expressionAtPath || expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); + } + return true; } + // TODO Lukas we could match all getters here as well if (typeof key !== 'string') return true; const properties = this.gettersByKey[key] || this.unmatchableGetters; for (const property of properties) { if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; } + if (this.prototypeExpression && !this.propertiesByKey[key]) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } return false; } @@ -154,7 +171,10 @@ export class ObjectEntity implements ExpressionEntity { const [key, ...subPath] = path; if (path.length > 1) { const expressionAtPath = this.getMemberExpression(key); - return !expressionAtPath || expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + if (expressionAtPath) { + return expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); + } + return true; } if (typeof key !== 'string') return true; @@ -163,6 +183,9 @@ export class ObjectEntity implements ExpressionEntity { for (const property of properties) { if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; } + if (this.prototypeExpression && !this.settersByKey[key]) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + } return false; } @@ -176,10 +199,10 @@ export class ObjectEntity implements ExpressionEntity { if (expressionAtPath) { return expressionAtPath.hasEffectsWhenCalledAtPath(path.slice(1), callOptions, context); } - if (path.length > 1) { - return true; + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenCalledAtPath(path, callOptions, context); } - return hasMemberEffectWhenCalled(objectMembers, key, this.included, callOptions, context); + return true; } include() { @@ -209,6 +232,9 @@ export class ObjectEntity implements ExpressionEntity { origin ); } + if (this.prototypeExpression) { + return this.prototypeExpression.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); + } return false; } diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts new file mode 100644 index 00000000000..c095b7c673d --- /dev/null +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -0,0 +1,77 @@ +import { CallOptions } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown } from '../../unknownValues'; +import { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import SpreadElement from '../SpreadElement'; +import { ExpressionEntity } from './Expression'; +import { ExpressionNode, IncludeChildren } from './Node'; + +export class ObjectMember implements ExpressionEntity { + included = false; + + constructor(private readonly object: ExpressionEntity, private readonly key: string) {} + + deoptimizePath(path: ObjectPath): void { + this.object.deoptimizePath([this.key, ...path]); + } + + // TODO Lukas think about all places where this might need to be implemented + deoptimizeProperties(): void {} + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.object.getLiteralValueAtPath([this.key, ...path], recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.object.getReturnExpressionWhenCalledAtPath( + [this.key, ...path], + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (path.length === 0) return false; + return this.object.hasEffectsWhenAccessedAtPath([this.key, ...path], context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return this.object.hasEffectsWhenAssignedAtPath([this.key, ...path], context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ): boolean { + return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { + this.included = true; + this.object.include(context, includeChildrenRecursively); + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } + + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): boolean { + return this.object.mayModifyThisWhenCalledAtPath([this.key, ...path], recursionTracker, origin); + } +} diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts new file mode 100644 index 00000000000..03fdd8fe48c --- /dev/null +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -0,0 +1,18 @@ +import { + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity } from './ObjectEntity'; + +export const OBJECT_PROTOTYPE = new ObjectEntity( + [ + { key: 'hasOwnProperty', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, + { key: 'isPrototypeOf', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, + { key: 'propertyIsEnumerable', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, + { key: 'toLocaleString', kind: 'init', property: METHOD_RETURNS_STRING }, + { key: 'toString', kind: 'init', property: METHOD_RETURNS_STRING }, + { key: 'valueOf', kind: 'init', property: METHOD_RETURNS_UNKNOWN } + ], + null +); diff --git a/src/ast/unknownValues.ts b/src/ast/unknownValues.ts index d4a89e1d167..e59b8417f3e 100644 --- a/src/ast/unknownValues.ts +++ b/src/ast/unknownValues.ts @@ -14,6 +14,8 @@ export class UnknownExpression implements ExpressionEntity { deoptimizePath() {} + deoptimizeProperties() {} + getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } @@ -22,15 +24,12 @@ export class UnknownExpression implements ExpressionEntity { return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath( - path: ObjectPath, - _context: HasEffectsContext - ) { - return path.length > 0 + hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { + return path.length > 0; } hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 0 + return path.length > 0; } hasEffectsWhenCalledAtPath( @@ -49,13 +48,16 @@ export class UnknownExpression implements ExpressionEntity { } } - mayModifyThisWhenCalledAtPath() { return true; } + mayModifyThisWhenCalledAtPath() { + return true; + } } export const UNKNOWN_EXPRESSION: ExpressionEntity = new UnknownExpression(); -export const UNDEFINED_EXPRESSION: ExpressionEntity = new class UndefinedExpression extends UnknownExpression { +// TODO Lukas maybe move this back to values +export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpression extends UnknownExpression { getLiteralValueAtPath() { return undefined; } -}; +})(); diff --git a/src/ast/values.ts b/src/ast/values.ts index d8f5644eff5..77bbe6aeaa1 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -2,7 +2,7 @@ import { CallOptions, NO_ARGS } from './CallOptions'; import { HasEffectsContext } from './ExecutionContext'; import { LiteralValue } from './nodes/Literal'; import { ExpressionEntity } from './nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION, UnknownExpression } from './unknownValues'; +import { UnknownExpression, UNKNOWN_EXPRESSION } from './unknownValues'; import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { @@ -110,7 +110,7 @@ const callsArgMutatesSelfReturnsArray: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends UnknownExpression { +export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends UnknownExpression { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); @@ -192,7 +192,7 @@ const callsArgReturnsNumber: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends UnknownExpression { +export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends UnknownExpression { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); @@ -223,7 +223,9 @@ const returnsString: RawMemberDescription = { } }; -// TODO Lukas instead use an object entity +// TODO Lukas This could just be the constant and above, we use an ObjectEntity with OBJECT_PROTOTYPE as prototype +// TODO Lukas Also, the name should reflect we assume neither getters nor setters and do not override builtins +// or just remove? export class UnknownObjectExpression extends UnknownExpression { included = false; diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 3386aa1777d..0df182b1c08 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -35,6 +35,8 @@ export default class Variable implements ExpressionEntity { deoptimizePath(_path: ObjectPath) {} + deoptimizeProperties() {} + getBaseVariableName(): string { return this.renderBaseName || this.renderName || this.name; } diff --git a/test/form/samples/modify-class-prototype/_expected.js b/test/form/samples/modify-class-prototype/_expected.js index 61e4f3c329f..8dd53679723 100644 --- a/test/form/samples/modify-class-prototype/_expected.js +++ b/test/form/samples/modify-class-prototype/_expected.js @@ -3,4 +3,12 @@ Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; + export { Retained }; diff --git a/test/form/samples/modify-class-prototype/main.js b/test/form/samples/modify-class-prototype/main.js index 38f8fa9abf0..1bf13d724dc 100644 --- a/test/form/samples/modify-class-prototype/main.js +++ b/test/form/samples/modify-class-prototype/main.js @@ -7,3 +7,18 @@ export class Retained {} Retained.prop = 42; Retained.prototype.prop = 43; Retained.prototype.prop2 = Retained.prototype.prop; + +class RemovedSuper { + prop() {} +} +class RemovedWithSuper extends RemovedSuper {} +RemovedWithSuper.prototype.prop = 42; +RemovedWithSuper.prototype.prop2 = 43; + +class RetainedSuper { + set prop(v) { + console.log('effect', v); + } +} +class RetainedWithSuper extends RetainedSuper {} +RetainedWithSuper.prototype.prop = 42; diff --git a/test/form/samples/side-effects-class-getters-setters/_expected.js b/test/form/samples/side-effects-class-getters-setters/_expected.js index a6beaf62970..25f4009032b 100644 --- a/test/form/samples/side-effects-class-getters-setters/_expected.js +++ b/test/form/samples/side-effects-class-getters-setters/_expected.js @@ -24,12 +24,14 @@ class RetainedSuper { class RetainedSub extends RetainedSuper {} RetainedSub.a; -class DeoptProto {} -unknown(DeoptProto.prototype); -DeoptProto.prototype.a; +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); class DeoptComputed { static get a() {} - static get [unknown]() { log(); } + static get [globalThis.unknown]() { log(); } } DeoptComputed.a; diff --git a/test/form/samples/side-effects-class-getters-setters/main.js b/test/form/samples/side-effects-class-getters-setters/main.js index b5c78edf787..971b7bb6676 100644 --- a/test/form/samples/side-effects-class-getters-setters/main.js +++ b/test/form/samples/side-effects-class-getters-setters/main.js @@ -59,12 +59,19 @@ RetainedSub.a; class RemovedSub extends RetainedSuper {} RemovedSub.b; -class DeoptProto {} -unknown(DeoptProto.prototype); -DeoptProto.prototype.a; +class RemovedProtoValue { + a = true; +} +if (!RemovedProtoValue.prototype.a) log(); + +class DeoptProto { + a = true; +} +globalThis.unknown(DeoptProto.prototype); +if (!DeoptProto.prototype.a) log(); class DeoptComputed { static get a() {} - static get [unknown]() { log(); } + static get [globalThis.unknown]() { log(); } } DeoptComputed.a; From d723df65bdf4689f4c5d0747752cce8d4ca5d7aa Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 23 Apr 2021 07:08:12 +0200 Subject: [PATCH 19/50] Improve coverage --- src/ast/nodes/shared/ObjectEntity.ts | 20 +++++++------------ .../shadowed-setters/_config.js | 3 +++ .../shadowed-setters/_expected.js | 8 ++++++++ .../shadowed-setters/main.js | 17 ++++++++++++++++ .../samples/undefined-properties/_config.js | 3 +-- .../samples/undefined-properties/_expected.js | 9 +++++++++ .../form/samples/undefined-properties/main.js | 7 +++++-- .../deoptimize-computed-keys/_config.js | 3 +++ .../samples/deoptimize-computed-keys/main.js | 14 +++++++++++++ 9 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/_config.js create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js create mode 100644 test/form/samples/property-setters-and-getters/shadowed-setters/main.js create mode 100644 test/function/samples/deoptimize-computed-keys/_config.js create mode 100644 test/function/samples/deoptimize-computed-keys/main.js diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 4629809a946..74e2e73380c 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,6 +1,6 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { HasEffectsContext } from '../../ExecutionContext'; import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; import { ObjectPath, @@ -9,9 +9,7 @@ import { UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; -import { ExpressionNode } from './Node'; export interface ObjectProperty { key: ObjectPathKey; @@ -23,7 +21,7 @@ type PropertyMap = Record; // TODO Lukas add a way to directly inject only propertiesByKey and create allProperties lazily/not export class ObjectEntity implements ExpressionEntity { - included = false; + included = true; private readonly allProperties: ExpressionEntity[] = []; private readonly deoptimizedPaths = new Set(); @@ -44,7 +42,9 @@ export class ObjectEntity implements ExpressionEntity { } deoptimizeObject(): void { - if (this.hasUnknownDeoptimizedProperty) return; + if (this.hasUnknownDeoptimizedProperty) { + return; + } this.hasUnknownDeoptimizedProperty = true; this.deoptimizeProperties(); for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { @@ -205,15 +205,9 @@ export class ObjectEntity implements ExpressionEntity { return true; } - include() { - this.included = true; - } + include() {} - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } - } + includeCallArguments() {} mayModifyThisWhenCalledAtPath( path: ObjectPath, diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js new file mode 100644 index 00000000000..f220613b295 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles setters shadowed by computed setters' +}; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js new file mode 100644 index 00000000000..2758779af18 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/_expected.js @@ -0,0 +1,8 @@ +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/property-setters-and-getters/shadowed-setters/main.js b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js new file mode 100644 index 00000000000..e5b1a9baf01 --- /dev/null +++ b/test/form/samples/property-setters-and-getters/shadowed-setters/main.js @@ -0,0 +1,17 @@ +const objRemoved = { + set value(v) { + console.log('shadowed'); + }, + set value(v) {} +}; + +objRemoved.value = 'removed'; + +const objRetained = { + set value(v) {}, + set [globalThis.unknown](v) { + console.log('effect'); + } +}; + +objRetained.value = 'retained'; diff --git a/test/form/samples/undefined-properties/_config.js b/test/form/samples/undefined-properties/_config.js index a89e4af5ef0..29b3cc673cb 100644 --- a/test/form/samples/undefined-properties/_config.js +++ b/test/form/samples/undefined-properties/_config.js @@ -1,4 +1,3 @@ module.exports = { - description: 'detects undefined properties', - expectedWarnings: ['EMPTY_BUNDLE'] + description: 'detects undefined properties' }; diff --git a/test/form/samples/undefined-properties/_expected.js b/test/form/samples/undefined-properties/_expected.js index 8b137891791..f97408e6838 100644 --- a/test/form/samples/undefined-properties/_expected.js +++ b/test/form/samples/undefined-properties/_expected.js @@ -1 +1,10 @@ +var a = { + b: { + c: 'd' + } +}; +console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); diff --git a/test/form/samples/undefined-properties/main.js b/test/form/samples/undefined-properties/main.js index e7df0af9514..bf2ad33721a 100644 --- a/test/form/samples/undefined-properties/main.js +++ b/test/form/samples/undefined-properties/main.js @@ -4,5 +4,8 @@ var a = { } }; -if (a.b.d) - console.log('yes'); \ No newline at end of file +if (a.b.d) console.log('removed'); +else console.log('retained'); + +if (a.c.d) console.log('retained for effect'); +else console.log('retained for effect'); \ No newline at end of file diff --git a/test/function/samples/deoptimize-computed-keys/_config.js b/test/function/samples/deoptimize-computed-keys/_config.js new file mode 100644 index 00000000000..2ac75a24810 --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes computed object property keys' +}; diff --git a/test/function/samples/deoptimize-computed-keys/main.js b/test/function/samples/deoptimize-computed-keys/main.js new file mode 100644 index 00000000000..273087a4e6a --- /dev/null +++ b/test/function/samples/deoptimize-computed-keys/main.js @@ -0,0 +1,14 @@ +var key1 = 'x' +var key2 = 'y' + +changeKeys(); + +var foo = { [key1]: true, [key2]: false }; + +assert.strictEqual(foo.x, false); +assert.strictEqual(foo.y, true); + +function changeKeys() { + key1 = 'y' + key2 = 'x' +} \ No newline at end of file From a2f02c18818f389154f4a177f431938c65d3f9de Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 23 Apr 2021 07:25:50 +0200 Subject: [PATCH 20/50] Fix class deoptimization in arrow functions via this/super --- src/ast/nodes/ClassBody.ts | 29 +++++++++++- src/ast/nodes/MethodDefinition.ts | 18 ++----- src/ast/nodes/PropertyDefinition.ts | 3 +- src/ast/nodes/Super.ts | 23 +++++++++ src/ast/nodes/ThisExpression.ts | 20 +++++++- src/ast/scopes/ClassBodyScope.ts | 18 +++++++ src/ast/variables/LocalVariable.ts | 2 +- src/ast/variables/ThisVariable.ts | 2 +- src/ast/variables/Variable.ts | 2 +- .../samples/class-method-access/_config.js | 3 ++ .../samples/class-method-access/_expected.js | 16 +++++++ test/form/samples/class-method-access/main.js | 30 ++++++++++++ .../literals-from-class-statics/_expected.js | 2 +- .../literals-from-class-statics/main.js | 2 +- .../static-class-property-calls/_config.js | 3 ++ .../static-class-property-calls/_expected.js | 21 +++++++++ .../static-class-property-calls/main.js | 23 +++++++++ .../samples/this-in-class-body/_config.js | 3 ++ .../samples/this-in-class-body/_expected.js | 37 +++++++++++++++ test/form/samples/this-in-class-body/main.js | 47 +++++++++++++++++++ .../deoptimize-computed-class-keys/_config.js | 3 ++ .../deoptimize-computed-class-keys/main.js | 17 +++++++ 22 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 test/form/samples/class-method-access/_config.js create mode 100644 test/form/samples/class-method-access/_expected.js create mode 100644 test/form/samples/class-method-access/main.js create mode 100644 test/form/samples/static-class-property-calls/_config.js create mode 100644 test/form/samples/static-class-property-calls/_expected.js create mode 100644 test/form/samples/static-class-property-calls/main.js create mode 100644 test/form/samples/this-in-class-body/_config.js create mode 100644 test/form/samples/this-in-class-body/_expected.js create mode 100644 test/form/samples/this-in-class-body/main.js create mode 100644 test/function/samples/deoptimize-computed-class-keys/_config.js create mode 100644 test/function/samples/deoptimize-computed-class-keys/main.js diff --git a/src/ast/nodes/ClassBody.ts b/src/ast/nodes/ClassBody.ts index cd0d21c907e..3f7c55ff760 100644 --- a/src/ast/nodes/ClassBody.ts +++ b/src/ast/nodes/ClassBody.ts @@ -1,15 +1,40 @@ +import { InclusionContext } from '../ExecutionContext'; import ClassBodyScope from '../scopes/ClassBodyScope'; import Scope from '../scopes/Scope'; import MethodDefinition from './MethodDefinition'; import * as NodeType from './NodeType'; import PropertyDefinition from './PropertyDefinition'; -import { NodeBase } from './shared/Node'; +import ClassNode from './shared/ClassNode'; +import { GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; export default class ClassBody extends NodeBase { body!: (MethodDefinition | PropertyDefinition)[]; + scope!: ClassBodyScope; type!: NodeType.tClassBody; createScope(parentScope: Scope) { - this.scope = new ClassBodyScope(parentScope); + this.scope = new ClassBodyScope(parentScope, this.parent as ClassNode, this.context); + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + this.included = true; + this.context.includeVariableInModule(this.scope.thisVariable); + for (const definition of this.body) { + definition.include(context, includeChildrenRecursively); + } + } + + parseNode(esTreeNode: GenericEsTreeNode) { + const body: NodeBase[] = (this.body = []); + for (const definition of esTreeNode.body) { + body.push( + new this.context.nodeConstructors[definition.type]( + definition, + this, + definition.static ? this.scope : this.scope.instanceScope + ) + ); + } + super.parseNode(esTreeNode); } } diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index 75a025fcaea..ea70fed9cc0 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -52,23 +52,17 @@ export default class MethodDefinition extends NodeBase { } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - return ( - this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context) - ); + if (this.kind === 'get' && path.length === 0) { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); } - return this.value.hasEffectsWhenAccessedAtPath(path); + return this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); - } if (this.kind === 'set') { return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); } - return this.value.hasEffectsWhenAccessedAtPath(path); + return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -76,9 +70,7 @@ export default class MethodDefinition extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ) { - return ( - path.length > 0 || this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, callOptions, context) - ); + return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); } mayModifyThisWhenCalledAtPath( diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index eac3ea58d48..1c1f7de26a4 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,7 +1,7 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; @@ -54,7 +54,6 @@ export default class PropertyDefinition extends NodeBase { return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); } - // TODO Lukas verify this is modifying this mayModifyThisWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index d09554da7bc..11f7522d28e 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -1,6 +1,29 @@ +import { ObjectPath } from '../utils/PathTracker'; +import ThisVariable from '../variables/ThisVariable'; import * as NodeType from './NodeType'; import { NodeBase } from './shared/Node'; export default class Super extends NodeBase { type!: NodeType.tSuper; + + variable!: ThisVariable; + private bound = false; + + bind() { + if (this.bound) return; + this.bound = true; + this.variable = this.scope.findVariable('this') as ThisVariable; + } + + deoptimizePath(path: ObjectPath) { + if (!this.bound) this.bind(); + this.variable.deoptimizePath(path); + } + + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index f68631adae6..9ea933a96d3 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -4,6 +4,7 @@ import ModuleScope from '../scopes/ModuleScope'; import { ObjectPath } from '../utils/PathTracker'; import ThisVariable from '../variables/ThisVariable'; import * as NodeType from './NodeType'; +import ClassNode from './shared/ClassNode'; import FunctionNode from './shared/FunctionNode'; import { NodeBase } from './shared/Node'; @@ -12,12 +13,19 @@ export default class ThisExpression extends NodeBase { variable!: ThisVariable; private alias!: string | null; + private bound = false; bind() { - super.bind(); + if (this.bound) return; + this.bound = true; this.variable = this.scope.findVariable('this') as ThisVariable; } + deoptimizePath(path: ObjectPath) { + if (!this.bound) this.bind(); + this.variable.deoptimizePath(path); + } + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 && this.variable.hasEffectsWhenAccessedAtPath(path, context); } @@ -26,6 +34,13 @@ export default class ThisExpression extends NodeBase { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } + include() { + if (!this.included) { + this.included = true; + this.context.includeVariableInModule(this.variable); + } + } + initialise() { this.alias = this.scope.findLexicalBoundary() instanceof ModuleScope ? this.context.moduleContext : null; @@ -40,6 +55,9 @@ export default class ThisExpression extends NodeBase { ); } for (let parent = this.parent; parent instanceof NodeBase; parent = parent.parent) { + if (parent instanceof ClassNode) { + break; + } if (parent instanceof FunctionNode) { parent.referencesThis = true; break; diff --git a/src/ast/scopes/ClassBodyScope.ts b/src/ast/scopes/ClassBodyScope.ts index ebda392188a..83952d39dc6 100644 --- a/src/ast/scopes/ClassBodyScope.ts +++ b/src/ast/scopes/ClassBodyScope.ts @@ -1,6 +1,24 @@ +import { AstContext } from '../../Module'; +import { ExpressionEntity } from '../nodes/shared/Expression'; +import LocalVariable from '../variables/LocalVariable'; +import ThisVariable from '../variables/ThisVariable'; import ChildScope from './ChildScope'; +import Scope from './Scope'; export default class ClassBodyScope extends ChildScope { + instanceScope: ChildScope; + thisVariable: LocalVariable; + + constructor(parent: Scope, classNode: ExpressionEntity, context: AstContext) { + super(parent); + this.variables.set( + 'this', + (this.thisVariable = new LocalVariable('this', null, classNode, context)) + ); + this.instanceScope = new ChildScope(this); + this.instanceScope.variables.set('this', new ThisVariable(context)); + } + findLexicalBoundary() { return this; } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index a1ac4c39f8a..95e5e9dba27 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -8,7 +8,7 @@ import * as NodeType from '../nodes/NodeType'; import { ExpressionEntity } from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 839c635f4f1..93f7f00f318 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -2,7 +2,7 @@ import { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 0df182b1c08..5738ddce9f4 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -8,7 +8,7 @@ import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION, UnknownValue } from '../unknownValues'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; export default class Variable implements ExpressionEntity { diff --git a/test/form/samples/class-method-access/_config.js b/test/form/samples/class-method-access/_config.js new file mode 100644 index 00000000000..d44dede48ae --- /dev/null +++ b/test/form/samples/class-method-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks side-effects when accessing class methods' +}; diff --git a/test/form/samples/class-method-access/_expected.js b/test/form/samples/class-method-access/_expected.js new file mode 100644 index 00000000000..034e5c35fe7 --- /dev/null +++ b/test/form/samples/class-method-access/_expected.js @@ -0,0 +1,16 @@ +console.log('retained'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} +Used.method.doesNotExist.throws; +Used.getter.doesNotExist.throws; +Used.getter.foo.doesNotExist.throws; +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; diff --git a/test/form/samples/class-method-access/main.js b/test/form/samples/class-method-access/main.js new file mode 100644 index 00000000000..199ab716dff --- /dev/null +++ b/test/form/samples/class-method-access/main.js @@ -0,0 +1,30 @@ +class Removed { + static get isTrue() { + return true; + } +} + +if (Removed.isTrue) console.log('retained'); +else console.log('removed'); + +class Used { + static method() {} + static get getter() { + return { foo: {} }; + } +} + +Used.method.doesNotExist; +Used.method.doesNotExist.throws; +Used.getter.doesNotExist; +Used.getter.doesNotExist.throws; +Used.getter.foo; +Used.getter.foo.doesNotExist; +Used.getter.foo.doesNotExist.throws; +Used.getter.hasOwnProperty('foo'); +Used.getter.foo.hasOwnProperty('bar'); +Used.getter.throws(); +Used.getter.foo.throws(); + +Used.method.reassigned = 1; +Used.getter.reassigned = 2; diff --git a/test/form/samples/literals-from-class-statics/_expected.js b/test/form/samples/literals-from-class-statics/_expected.js index 849a60add18..09ce1e861ac 100644 --- a/test/form/samples/literals-from-class-statics/_expected.js +++ b/test/form/samples/literals-from-class-statics/_expected.js @@ -2,7 +2,7 @@ log("t"); log("x"); class Undef { - static y; + static 'y'; } if (Undef.y) log("y"); diff --git a/test/form/samples/literals-from-class-statics/main.js b/test/form/samples/literals-from-class-statics/main.js index 13c231108ff..a8cdcb481d4 100644 --- a/test/form/samples/literals-from-class-statics/main.js +++ b/test/form/samples/literals-from-class-statics/main.js @@ -10,7 +10,7 @@ if (!Static.t()) log("!t"); if (Static.x) log("x"); class Undef { - static y; + static 'y'; } if (Undef.y) log("y"); diff --git a/test/form/samples/static-class-property-calls/_config.js b/test/form/samples/static-class-property-calls/_config.js new file mode 100644 index 00000000000..2983907efde --- /dev/null +++ b/test/form/samples/static-class-property-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles effects when calling static class properties' +}; diff --git a/test/form/samples/static-class-property-calls/_expected.js b/test/form/samples/static-class-property-calls/_expected.js new file mode 100644 index 00000000000..e2589637c16 --- /dev/null +++ b/test/form/samples/static-class-property-calls/_expected.js @@ -0,0 +1,21 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +console.log('retained'); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/static-class-property-calls/main.js b/test/form/samples/static-class-property-calls/main.js new file mode 100644 index 00000000000..4259adcdb71 --- /dev/null +++ b/test/form/samples/static-class-property-calls/main.js @@ -0,0 +1,23 @@ +class Foo { + static isTrue = () => true; + static noEffect = () => {}; + static effect = () => console.log('effect'); + static missing; +} + +if (Foo.isTrue()) console.log('retained'); +else console.log('removed'); +Foo.noEffect(); +Foo.effect(); +Foo.missing(); + +class Bar { + static flag = false + static mutate = function() { + this.flag = true; + } +} + +Bar.mutate(); +if (Bar.flag) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/this-in-class-body/_config.js b/test/form/samples/this-in-class-body/_config.js new file mode 100644 index 00000000000..1ade8977cfe --- /dev/null +++ b/test/form/samples/this-in-class-body/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'uses the correct "this" value in class properties' +}; diff --git a/test/form/samples/this-in-class-body/_expected.js b/test/form/samples/this-in-class-body/_expected.js new file mode 100644 index 00000000000..ff7c88e7839 --- /dev/null +++ b/test/form/samples/this-in-class-body/_expected.js @@ -0,0 +1,37 @@ +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +console.log('retained'); diff --git a/test/form/samples/this-in-class-body/main.js b/test/form/samples/this-in-class-body/main.js new file mode 100644 index 00000000000..6fa4fc5ed65 --- /dev/null +++ b/test/form/samples/this-in-class-body/main.js @@ -0,0 +1,47 @@ +class Unused { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Unused.mutate(); + +class Used { + static flag = false + static mutate = () => { + this.flag = true; + } +} +Used.mutate(); + +if (Used.flag) console.log('retained'); +else console.log('unimportant'); + +class InstanceMutation { + static flag = false + flag = false + mutate = () => { + this.flag = true; + } +} +(new InstanceMutation).mutate(); + +if (InstanceMutation.flag) console.log('removed'); +else console.log('retained'); + +class UsedSuper { + static flag = false +} +class UsedWithSuper extends UsedSuper{ + static mutate = () => { + super.flag = true; + } +} +UsedWithSuper.mutate(); + +if (UsedWithSuper.flag) console.log('retained'); +else console.log('unimportant'); + +// Assignments via "super" do NOT mutate the super class! +if (UsedSuper.flag) console.log('removed'); +else console.log('retained'); diff --git a/test/function/samples/deoptimize-computed-class-keys/_config.js b/test/function/samples/deoptimize-computed-class-keys/_config.js new file mode 100644 index 00000000000..dfe1e1e59b7 --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes computed class property keys' +}; diff --git a/test/function/samples/deoptimize-computed-class-keys/main.js b/test/function/samples/deoptimize-computed-class-keys/main.js new file mode 100644 index 00000000000..bad340b2305 --- /dev/null +++ b/test/function/samples/deoptimize-computed-class-keys/main.js @@ -0,0 +1,17 @@ +var key1 = 'x'; +var key2 = 'y'; + +changeKeys(); + +class Foo { + static [key1] = true; + static [key2] = false; +} + +assert.strictEqual(Foo.x, false); +assert.strictEqual(Foo.y, true); + +function changeKeys() { + key1 = 'y'; + key2 = 'x'; +} From f89085c536e2580afc3db30b914638b1c397717f Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 23 Apr 2021 15:02:25 +0200 Subject: [PATCH 21/50] Simplify and merge property and method definition --- src/ast/nodes/MethodDefinition.ts | 88 +---------- src/ast/nodes/Property.ts | 142 ++---------------- src/ast/nodes/shared/MethodBase.ts | 98 ++++++++++++ test/form/samples/class-method-access/main.js | 4 + 4 files changed, 116 insertions(+), 216 deletions(-) create mode 100644 src/ast/nodes/shared/MethodBase.ts diff --git a/src/ast/nodes/MethodDefinition.ts b/src/ast/nodes/MethodDefinition.ts index ea70fed9cc0..07b0e88366a 100644 --- a/src/ast/nodes/MethodDefinition.ts +++ b/src/ast/nodes/MethodDefinition.ts @@ -1,95 +1,13 @@ -import { CallOptions, NO_ARGS } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../unknownValues'; -import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import FunctionExpression from './FunctionExpression'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; -export default class MethodDefinition extends NodeBase { - computed!: boolean; +export default class MethodDefinition extends MethodBase { key!: ExpressionNode | PrivateIdentifier; kind!: 'constructor' | 'method' | 'get' | 'set'; static!: boolean; type!: NodeType.tMethodDefinition; value!: FunctionExpression; - - private accessedValue: ExpressionEntity | null = null; - private accessorCallOptions: CallOptions = { - args: NO_ARGS, - withNew: false - }; - - deoptimizePath(path: ObjectPath) { - this.getAccessedValue().deoptimizePath(path); - } - - getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return this.getAccessedValue().getLiteralValueAtPath(path, recursionTracker, origin); - } - - getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity { - return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( - path, - recursionTracker, - origin - ); - } - - hasEffects(context: HasEffectsContext) { - return this.key.hasEffects(context); - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get' && path.length === 0) { - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'set') { - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); - } - - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): boolean { - return this.getAccessedValue().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - - private getAccessedValue(): ExpressionEntity { - if (this.accessedValue === null) { - if (this.kind === 'get') { - this.accessedValue = UNKNOWN_EXPRESSION; - return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH)); - } else { - return (this.accessedValue = this.value); - } - } - return this.accessedValue; - } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index f839b30819c..8222e0dffc4 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -1,37 +1,23 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; -import { CallOptions, NO_ARGS } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../unknownValues'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UnknownKey -} from '../utils/PathTracker'; +import { UNKNOWN_EXPRESSION } from '../unknownValues'; +import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import MethodBase from './shared/MethodBase'; +import { ExpressionNode } from './shared/Node'; import { PatternNode } from './shared/Pattern'; -export default class Property extends NodeBase implements DeoptimizableEntity, PatternNode { - computed!: boolean; +export default class Property extends MethodBase implements PatternNode { key!: ExpressionNode; kind!: 'init' | 'get' | 'set'; method!: boolean; shorthand!: boolean; type!: NodeType.tProperty; - value!: ExpressionNode | (ExpressionNode & PatternNode); - private accessorCallOptions: CallOptions = { - args: NO_ARGS, - withNew: false - }; private declarationInit: ExpressionEntity | null = null; - private returnExpression: ExpressionEntity | null = null; bind() { super.bind(); @@ -45,108 +31,14 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P return (this.value as PatternNode).declare(kind, UNKNOWN_EXPRESSION); } - // As getter properties directly receive their values from fixed function - // expressions, there is no known situation where a getter is deoptimized. - deoptimizeCache(): void {} - - deoptimizePath(path: ObjectPath) { - if (this.kind === 'get') { - this.getReturnExpression().deoptimizePath(path); - } else { - this.value.deoptimizePath(path); - } - } - - getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - if (this.kind === 'get') { - return this.getReturnExpression().getLiteralValueAtPath(path, recursionTracker, origin); - } - return this.value.getLiteralValueAtPath(path, recursionTracker, origin); - } - - getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity { - if (this.kind === 'get') { - return this.getReturnExpression().getReturnExpressionWhenCalledAtPath( - path, - recursionTracker, - origin - ); - } - return this.value.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - } - hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions).propertyReadSideEffects; - return this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always' || + const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) + .propertyReadSideEffects; + return ( + (this.parent.type === 'ObjectPattern' && propertyReadSideEffects === 'always') || this.key.hasEffects(context) || - this.value.hasEffects(context); - } - - // TODO Lukas why do we have recursion tracking here? - // TODO Lukas can we simplify things like with MethodDefinition? - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return ( - this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context) || - (path.length > 0 && this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context)) - ); - } - return this.value.hasEffectsWhenAccessedAtPath(path, context); - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (this.kind === 'get') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context); - } - if (this.kind === 'set') { - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); - } - return this.value.hasEffectsWhenAssignedAtPath(path, context); - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (this.kind === 'get') { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); - return this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context); - } - return this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); - } - - mayModifyThisWhenCalledAtPath(path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity): boolean { - if (this.kind === 'get') { - return this.getReturnExpression().mayModifyThisWhenCalledAtPath( - path, - recursionTracker, - origin - ); - } - return this.value.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); + this.value.hasEffects(context) + ); } render(code: MagicString, options: RenderOptions) { @@ -155,16 +47,4 @@ export default class Property extends NodeBase implements DeoptimizableEntity, P } this.value.render(code, options, { isShorthandProperty: this.shorthand }); } - - private getReturnExpression(): ExpressionEntity { - if (this.returnExpression === null) { - this.returnExpression = UNKNOWN_EXPRESSION; - return (this.returnExpression = this.value.getReturnExpressionWhenCalledAtPath( - EMPTY_PATH, - SHARED_RECURSION_TRACKER, - this - )); - } - return this.returnExpression; - } } diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts new file mode 100644 index 00000000000..1d48d969195 --- /dev/null +++ b/src/ast/nodes/shared/MethodBase.ts @@ -0,0 +1,98 @@ +import { CallOptions, NO_ARGS } from '../../CallOptions'; +import { DeoptimizableEntity } from '../../DeoptimizableEntity'; +import { HasEffectsContext } from '../../ExecutionContext'; +import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../../unknownValues'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; +import PrivateIdentifier from '../PrivateIdentifier'; +import { ExpressionEntity } from './Expression'; +import { ExpressionNode, NodeBase } from './Node'; +import { PatternNode } from './Pattern'; + +export default class MethodBase extends NodeBase implements DeoptimizableEntity { + computed!: boolean; + key!: ExpressionNode | PrivateIdentifier; + kind!: 'constructor' | 'method' | 'init' | 'get' | 'set'; + value!: ExpressionNode | (ExpressionNode & PatternNode); + + private accessedValue: ExpressionEntity | null = null; + private accessorCallOptions: CallOptions = { + args: NO_ARGS, + withNew: false + }; + + // As getter properties directly receive their values from fixed function + // expressions, there is no known situation where a getter is deoptimized. + deoptimizeCache(): void {} + + deoptimizePath(path: ObjectPath) { + this.getAccessedValue().deoptimizePath(path); + } + + getLiteralValueAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return this.getAccessedValue().getLiteralValueAtPath(path, recursionTracker, origin); + } + + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): ExpressionEntity { + return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( + path, + recursionTracker, + origin + ); + } + + hasEffects(context: HasEffectsContext) { + return this.key.hasEffects(context); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'get' && path.length === 0) { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); + } + return this.getAccessedValue().hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + if (this.kind === 'set') { + return this.value.hasEffectsWhenCalledAtPath(EMPTY_PATH, this.accessorCallOptions, context); + } + return this.getAccessedValue().hasEffectsWhenAssignedAtPath(path, context); + } + + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { + return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); + } + + mayModifyThisWhenCalledAtPath( + path: ObjectPath, + recursionTracker: PathTracker, + origin: DeoptimizableEntity + ): boolean { + return this.getAccessedValue().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); + } + + protected getAccessedValue(): ExpressionEntity { + if (this.accessedValue === null) { + if (this.kind === 'get') { + this.accessedValue = UNKNOWN_EXPRESSION; + return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH, + SHARED_RECURSION_TRACKER, + this)); + } else { + return (this.accessedValue = this.value); + } + } + return this.accessedValue; + } +} diff --git a/test/form/samples/class-method-access/main.js b/test/form/samples/class-method-access/main.js index 199ab716dff..25e980dc120 100644 --- a/test/form/samples/class-method-access/main.js +++ b/test/form/samples/class-method-access/main.js @@ -28,3 +28,7 @@ Used.getter.foo.throws(); Used.method.reassigned = 1; Used.getter.reassigned = 2; + +class ValueEffect { + static foo +} \ No newline at end of file From 6d70426c63671b0fde91f6258adc99001747dfd2 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 23 Apr 2021 15:19:14 +0200 Subject: [PATCH 22/50] Improve coverage --- src/ast/nodes/Identifier.ts | 4 +--- src/ast/nodes/Super.ts | 2 +- src/ast/nodes/ThisExpression.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 1a4995d7ee6..4f34a768352 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -146,9 +146,7 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - return this.variable - ? this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) - : true; + return this.variable!.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } render( diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index 11f7522d28e..19ecad28462 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -16,7 +16,7 @@ export default class Super extends NodeBase { } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); + this.bind(); this.variable.deoptimizePath(path); } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index 9ea933a96d3..55dbef817fd 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -22,7 +22,7 @@ export default class ThisExpression extends NodeBase { } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); + this.bind(); this.variable.deoptimizePath(path); } From 4eff4ed79e573728e91f45c3cde6412b4c151037 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 24 Apr 2021 20:31:34 +0200 Subject: [PATCH 23/50] Replace deoptimizeProperties by deoptimizing a double unknown path --- src/ast/nodes/MemberExpression.ts | 15 +++++------ src/ast/nodes/ObjectExpression.ts | 2 +- src/ast/nodes/shared/ClassNode.ts | 2 +- src/ast/nodes/shared/Expression.ts | 4 --- src/ast/nodes/shared/MethodTypes.ts | 2 -- src/ast/nodes/shared/MultiExpression.ts | 6 ----- src/ast/nodes/shared/Node.ts | 2 -- src/ast/nodes/shared/ObjectEntity.ts | 26 ++++++------------- src/ast/nodes/shared/ObjectMember.ts | 3 --- src/ast/unknownValues.ts | 2 -- src/ast/variables/Variable.ts | 2 -- .../samples/nested-deoptimization/_config.js | 3 +++ .../nested-deoptimization/_expected.js | 11 ++++++++ .../samples/nested-deoptimization/main.js | 13 ++++++++++ .../deoptimize-computed-class-keys/_config.js | 3 ++- 15 files changed, 45 insertions(+), 51 deletions(-) create mode 100644 test/form/samples/nested-deoptimization/_config.js create mode 100644 test/form/samples/nested-deoptimization/_expected.js create mode 100644 test/form/samples/nested-deoptimization/main.js diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index ea34bbafa66..474c6b8fc90 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -119,18 +119,14 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); + this.bind(); if (path.length === 0) this.disallowNamespaceReassignment(); if (this.variable) { this.variable.deoptimizePath(path); } else { const propertyKey = this.getPropertyKey(); - if (propertyKey === UnknownKey) { - this.object.deoptimizePath(UNKNOWN_PATH); - } else { - this.wasPathDeoptimizedWhileOptimized = true; - this.object.deoptimizePath([propertyKey, ...path]); - } + this.wasPathDeoptimizedWhileOptimized = true; + this.object.deoptimizePath([propertyKey, ...path]); } } @@ -139,7 +135,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); + this.bind(); if (this.variable !== null) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -156,7 +152,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - if (!this.bound) this.bind(); + this.bind(); if (this.variable !== null) { return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); } @@ -238,6 +234,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ) { + this.bind(); if (this.variable) { return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); } diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index c960509b9c7..d69357aa920 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -29,7 +29,7 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE private objectEntity: ObjectEntity | null = null; deoptimizeCache() { - this.getObjectEntity().deoptimizeObject(); + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 0867d2446ed..65c3213466f 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -37,7 +37,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { } deoptimizeCache() { - this.getObjectEntity().deoptimizeObject(); + this.getObjectEntity().deoptimizeAllProperties(); } deoptimizePath(path: ObjectPath) { diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index c8612bcd5cd..48614f18c30 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -10,10 +10,6 @@ import { ExpressionNode, IncludeChildren } from './Node'; export interface ExpressionEntity extends WritableEntity { included: boolean; - /** - * Assume existing properties have been mutated but do not assume properties have been added/overwritten. Important for mutations of objects with prototypes. - */ - deoptimizeProperties():void /** * If possible it returns a stringifyable literal value for this node that can be used * for inlining or comparing values. diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index aa1a76927b9..d0c7c80af0a 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -28,8 +28,6 @@ class Method implements ExpressionEntity { deoptimizePath(): void {} - deoptimizeProperties(): void {} - getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index 0c47a8c6a35..0a6943f9d7d 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -21,12 +21,6 @@ export class MultiExpression implements ExpressionEntity { } } - deoptimizeProperties() { - for (const expression of this.expressions) { - expression.deoptimizeProperties(); - } - } - getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index aa17475ee7c..71323ab7c78 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -151,8 +151,6 @@ export class NodeBase implements ExpressionNode { deoptimizePath(_path: ObjectPath) {} - deoptimizeProperties() {} - getLiteralValueAtPath( _path: ObjectPath, _recursionTracker: PathTracker, diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 74e2e73380c..9052168820d 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -2,13 +2,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; -import { - ObjectPath, - ObjectPathKey, - PathTracker, - UnknownKey, - UNKNOWN_PATH -} from '../../utils/PathTracker'; +import { ObjectPath, ObjectPathKey, PathTracker, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; import { ExpressionEntity } from './Expression'; export interface ObjectProperty { @@ -41,12 +35,16 @@ export class ObjectEntity implements ExpressionEntity { this.buildPropertyMaps(properties); } - deoptimizeObject(): void { + deoptimizeAllProperties(): void { if (this.hasUnknownDeoptimizedProperty) { return; } this.hasUnknownDeoptimizedProperty = true; - this.deoptimizeProperties(); + for (const property of this.allProperties) { + property.deoptimizePath(UNKNOWN_PATH); + } + // While the prototype itself cannot be mutated, each property can + this.prototypeExpression?.deoptimizePath([UnknownKey, UnknownKey]); for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); @@ -59,7 +57,7 @@ export class ObjectEntity implements ExpressionEntity { const key = path[0]; if (path.length === 1) { if (typeof key !== 'string') { - this.deoptimizeObject(); + this.deoptimizeAllProperties(); return; } if (!this.deoptimizedPaths.has(key)) { @@ -76,7 +74,6 @@ export class ObjectEntity implements ExpressionEntity { } } - // TODO Lukas verify again why we need to handle the path.length === 1 case here const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); for (const property of typeof key === 'string' ? (this.propertiesByKey[key] || this.unmatchableProperties).concat( @@ -87,13 +84,6 @@ export class ObjectEntity implements ExpressionEntity { } } - deoptimizeProperties(): void { - for (const property of this.allProperties) { - property.deoptimizePath(UNKNOWN_PATH); - } - this.prototypeExpression?.deoptimizeProperties(); - } - getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index c095b7c673d..eebe2eaae92 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -16,9 +16,6 @@ export class ObjectMember implements ExpressionEntity { this.object.deoptimizePath([this.key, ...path]); } - // TODO Lukas think about all places where this might need to be implemented - deoptimizeProperties(): void {} - getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/unknownValues.ts b/src/ast/unknownValues.ts index e59b8417f3e..002cb4cb51c 100644 --- a/src/ast/unknownValues.ts +++ b/src/ast/unknownValues.ts @@ -14,8 +14,6 @@ export class UnknownExpression implements ExpressionEntity { deoptimizePath() {} - deoptimizeProperties() {} - getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 5738ddce9f4..3deb48e22a8 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -35,8 +35,6 @@ export default class Variable implements ExpressionEntity { deoptimizePath(_path: ObjectPath) {} - deoptimizeProperties() {} - getBaseVariableName(): string { return this.renderBaseName || this.renderName || this.name; } diff --git a/test/form/samples/nested-deoptimization/_config.js b/test/form/samples/nested-deoptimization/_config.js new file mode 100644 index 00000000000..440f76d3543 --- /dev/null +++ b/test/form/samples/nested-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of nested properties' +}; diff --git a/test/form/samples/nested-deoptimization/_expected.js b/test/form/samples/nested-deoptimization/_expected.js new file mode 100644 index 00000000000..04bb1a1620c --- /dev/null +++ b/test/form/samples/nested-deoptimization/_expected.js @@ -0,0 +1,11 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/nested-deoptimization/main.js b/test/form/samples/nested-deoptimization/main.js new file mode 100644 index 00000000000..df9ff8d41f7 --- /dev/null +++ b/test/form/samples/nested-deoptimization/main.js @@ -0,0 +1,13 @@ +const obj = { + foo: { prop: true }, + bar: { otherProp: true }, + prop: true +}; +obj[globalThis.unknown].prop = false; + +if (obj.foo.prop) console.log('retained'); +else console.log('also retained'); +if (obj.bar.otherProp) console.log('retained'); +else console.log('removed'); +if (obj.prop) console.log('retained'); +else console.log('removed'); diff --git a/test/function/samples/deoptimize-computed-class-keys/_config.js b/test/function/samples/deoptimize-computed-class-keys/_config.js index dfe1e1e59b7..53e617bd7fe 100644 --- a/test/function/samples/deoptimize-computed-class-keys/_config.js +++ b/test/function/samples/deoptimize-computed-class-keys/_config.js @@ -1,3 +1,4 @@ module.exports = { - description: 'deoptimizes computed class property keys' + description: 'deoptimizes computed class property keys', + minNodeVersion: 12 }; From dd5f73b8e334c046f87834b2f5fb916b4cdc25bc Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 24 Apr 2021 22:20:06 +0200 Subject: [PATCH 24/50] Assume functions can add getters to parameters --- src/ast/nodes/AssignmentPattern.ts | 1 + src/ast/nodes/CallExpression.ts | 56 ++++++++++--------- src/ast/nodes/shared/ObjectEntity.ts | 12 ++-- .../form/samples/nested-member-access/main.js | 4 -- .../{_expected/es.js => _expected.js} | 0 .../skips-dead-branches/_expected/amd.js | 9 --- .../skips-dead-branches/_expected/cjs.js | 7 --- .../skips-dead-branches/_expected/iife.js | 10 ---- .../skips-dead-branches/_expected/system.js | 14 ----- .../skips-dead-branches/_expected/umd.js | 12 ---- .../deoptimize-assumes-getters/_config.js | 3 + .../deoptimize-assumes-getters/main.js | 14 +++++ 12 files changed, 55 insertions(+), 87 deletions(-) rename test/form/samples/skips-dead-branches/{_expected/es.js => _expected.js} (100%) delete mode 100644 test/form/samples/skips-dead-branches/_expected/amd.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/cjs.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/iife.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/system.js delete mode 100644 test/form/samples/skips-dead-branches/_expected/umd.js create mode 100644 test/function/samples/deoptimize-assumes-getters/_config.js create mode 100644 test/function/samples/deoptimize-assumes-getters/main.js diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 06d6ea3268b..fc2c92d6b9e 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -21,6 +21,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.addExportedVariables(variables, exportNamesByVariable); } + // TODO Lukas make all current bind deoptimizations lazy bind() { super.bind(); this.left.deoptimizePath(EMPTY_PATH); diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index ec853a8be5d..944cd1890cf 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -31,6 +31,7 @@ import { import SpreadElement from './SpreadElement'; import Super from './Super'; +// TODO Lukas see which deoptimizations could be applied lazily, just like assignments export default class CallExpression extends NodeBase implements DeoptimizableEntity { arguments!: (ExpressionNode | SpreadElement)[]; callee!: ExpressionNode | Super; @@ -38,6 +39,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt type!: NodeType.tCallExpression; private callOptions!: CallOptions; + private deoptimized = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private returnExpression: ExpressionEntity | null = null; private wasPathDeoptmizedWhileOptimized = false; @@ -68,25 +70,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt ); } } - // ensure the returnExpression is set for the tree-shaking passes - this.getReturnExpression(SHARED_RECURSION_TRACKER); - if ( - this.callee instanceof MemberExpression && - !this.callee.variable && - this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER, this) - ) { - this.callee.object.deoptimizePath(UNKNOWN_PATH); - } - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } } deoptimizeCache() { if (this.returnExpression !== UNKNOWN_EXPRESSION) { this.returnExpression = null; - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); + const returnExpression = this.getReturnExpression(); const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; if (returnExpression !== UNKNOWN_EXPRESSION) { // We need to replace here because it is possible new expressions are added @@ -101,10 +90,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt expression.deoptimizeCache(); } } - if ( - this.callee instanceof MemberExpression && - !this.callee.variable - ) { + if (this.callee instanceof MemberExpression && !this.callee.variable) { this.callee.object.deoptimizePath(UNKNOWN_PATH); } } @@ -114,7 +100,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt const trackedEntities = this.context.deoptimizationTracker.getEntities(path); if (trackedEntities.has(this)) return; trackedEntities.add(this); - const returnExpression = this.getReturnExpression(SHARED_RECURSION_TRACKER); + const returnExpression = this.getReturnExpression(); if (returnExpression !== UNKNOWN_EXPRESSION) { this.wasPathDeoptmizedWhileOptimized = true; returnExpression.deoptimizePath(path); @@ -166,6 +152,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -185,7 +172,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt const trackedExpressions = context.accessed.getEntities(path); if (trackedExpressions.has(this)) return false; trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAccessedAtPath(path, context); + return this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { @@ -193,7 +180,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt const trackedExpressions = context.assigned.getEntities(path); if (trackedExpressions.has(this)) return false; trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenAssignedAtPath(path, context); + return this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -207,10 +194,11 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt ).getEntities(path, callOptions); if (trackedExpressions.has(this)) return false; trackedExpressions.add(this); - return this.returnExpression!.hasEffectsWhenCalledAtPath(path, callOptions, context); + return this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (includeChildrenRecursively) { super.include(context, includeChildrenRecursively); if ( @@ -225,8 +213,9 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.callee.include(context, false); } this.callee.includeCallArguments(context, this.arguments); - if (!this.returnExpression!.included) { - this.returnExpression!.include(context, false); + const returnExpression = this.getReturnExpression(); + if (!returnExpression.included) { + returnExpression.include(context, false); } } @@ -280,7 +269,24 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - private getReturnExpression(recursionTracker: PathTracker): ExpressionEntity { + private applyDeoptimizations() { + this.deoptimized = true; + if ( + this.callee instanceof MemberExpression && + !this.callee.variable && + this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER, this) + ) { + this.callee.object.deoptimizePath(UNKNOWN_PATH); + } + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } + + private getReturnExpression( + recursionTracker: PathTracker = SHARED_RECURSION_TRACKER + ): ExpressionEntity { if (this.returnExpression === null) { this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.callee.getReturnExpressionWhenCalledAtPath( diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 9052168820d..15d2fac56d2 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -18,7 +18,7 @@ export class ObjectEntity implements ExpressionEntity { included = true; private readonly allProperties: ExpressionEntity[] = []; - private readonly deoptimizedPaths = new Set(); + private readonly deoptimizedPaths: Record = Object.create(null); private readonly expressionsToBeDeoptimizedByKey: Record< string, DeoptimizableEntity[] @@ -60,8 +60,8 @@ export class ObjectEntity implements ExpressionEntity { this.deoptimizeAllProperties(); return; } - if (!this.deoptimizedPaths.has(key)) { - this.deoptimizedPaths.add(key); + if (!this.deoptimizedPaths[key]) { + this.deoptimizedPaths[key] = true; // we only deoptimizeCache exact matches as in all other cases, // we do not return a literal value or return expression @@ -144,8 +144,8 @@ export class ObjectEntity implements ExpressionEntity { return true; } - // TODO Lukas we could match all getters here as well - if (typeof key !== 'string') return true; + // TODO Lukas we could match all getters for unknown paths as well as well + if (typeof key !== 'string' || this.hasUnknownDeoptimizedProperty) return true; const properties = this.gettersByKey[key] || this.unmatchableGetters; for (const property of properties) { @@ -262,7 +262,7 @@ export class ObjectEntity implements ExpressionEntity { if ( this.hasUnknownDeoptimizedProperty || typeof key !== 'string' || - this.deoptimizedPaths.has(key) + this.deoptimizedPaths[key] ) { return UNKNOWN_EXPRESSION; } diff --git a/test/form/samples/nested-member-access/main.js b/test/form/samples/nested-member-access/main.js index 323ca9e7885..63a3efcc821 100644 --- a/test/form/samples/nested-member-access/main.js +++ b/test/form/samples/nested-member-access/main.js @@ -10,10 +10,6 @@ const removedResult3 = removed3.foo; const removed4 = !{}; const removedResult4 = removed4.foo; -let removed5a; -const removed5b = removed5a = {}; -const removedResult5 = removed5b.foo; - const removed6 = 1 + 2; const removedResult6 = removed6.foo; diff --git a/test/form/samples/skips-dead-branches/_expected/es.js b/test/form/samples/skips-dead-branches/_expected.js similarity index 100% rename from test/form/samples/skips-dead-branches/_expected/es.js rename to test/form/samples/skips-dead-branches/_expected.js diff --git a/test/form/samples/skips-dead-branches/_expected/amd.js b/test/form/samples/skips-dead-branches/_expected/amd.js deleted file mode 100644 index c192900373e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/amd.js +++ /dev/null @@ -1,9 +0,0 @@ -define(function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}); diff --git a/test/form/samples/skips-dead-branches/_expected/cjs.js b/test/form/samples/skips-dead-branches/_expected/cjs.js deleted file mode 100644 index b3e974bb4e6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/cjs.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -function bar () { - console.log( 'this should be included' ); -} - -bar(); diff --git a/test/form/samples/skips-dead-branches/_expected/iife.js b/test/form/samples/skips-dead-branches/_expected/iife.js deleted file mode 100644 index 3b894d92a46..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/iife.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}()); diff --git a/test/form/samples/skips-dead-branches/_expected/system.js b/test/form/samples/skips-dead-branches/_expected/system.js deleted file mode 100644 index 606d00fb15e..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/system.js +++ /dev/null @@ -1,14 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - - } - }; -}); diff --git a/test/form/samples/skips-dead-branches/_expected/umd.js b/test/form/samples/skips-dead-branches/_expected/umd.js deleted file mode 100644 index 06df11106d6..00000000000 --- a/test/form/samples/skips-dead-branches/_expected/umd.js +++ /dev/null @@ -1,12 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function bar () { - console.log( 'this should be included' ); - } - - bar(); - -}))); diff --git a/test/function/samples/deoptimize-assumes-getters/_config.js b/test/function/samples/deoptimize-assumes-getters/_config.js new file mode 100644 index 00000000000..c04d33fbc9b --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'assume full deoptimization may inject side-effectful getters' +}; diff --git a/test/function/samples/deoptimize-assumes-getters/main.js b/test/function/samples/deoptimize-assumes-getters/main.js new file mode 100644 index 00000000000..c11b529a8ff --- /dev/null +++ b/test/function/samples/deoptimize-assumes-getters/main.js @@ -0,0 +1,14 @@ +const obj = {}; +addGetter(obj); + +let mutated = false; +obj.prop; +assert.ok(mutated); + +function addGetter(o) { + Object.defineProperty(o, 'prop', { + get() { + mutated = true; + } + }); +} From 333116a9e5af69c2f59ac6efe57578e38ef6a0f3 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 25 Apr 2021 06:53:55 +0200 Subject: [PATCH 25/50] Improve class.prototype handling --- src/ast/nodes/CallExpression.ts | 1 - src/ast/nodes/Identifier.ts | 6 +-- src/ast/nodes/MemberExpression.ts | 9 +++-- src/ast/nodes/PropertyDefinition.ts | 8 ++++ src/ast/nodes/shared/ClassNode.ts | 8 ++-- src/ast/nodes/shared/ObjectEntity.ts | 17 +++++++- src/ast/nodes/shared/ObjectMember.ts | 17 ++------ .../_expected.js | 1 + .../main.js | 1 + .../super-class-prototype-access/_config.js | 3 ++ .../super-class-prototype-access/_expected.js | 8 ++++ .../super-class-prototype-access/main.js | 11 +++++ .../_config.js | 3 ++ .../_expected.js | 31 ++++++++++++++ .../super-class-prototype-assignment/main.js | 40 +++++++++++++++++++ .../super-class-prototype-calls/_config.js | 3 ++ .../super-class-prototype-calls/_expected.js | 23 +++++++++++ .../super-class-prototype-calls/main.js | 25 ++++++++++++ .../super-class-prototype-values/_config.js | 3 ++ .../super-class-prototype-values/_expected.js | 15 +++++++ .../super-class-prototype-values/main.js | 28 +++++++++++++ 21 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 test/form/samples/super-classes/super-class-prototype-access/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-access/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-access/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-assignment/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-calls/main.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/_config.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/_expected.js create mode 100644 test/form/samples/super-classes/super-class-prototype-values/main.js diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 944cd1890cf..3171d8c4ba4 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -31,7 +31,6 @@ import { import SpreadElement from './SpreadElement'; import Super from './Super'; -// TODO Lukas see which deoptimizations could be applied lazily, just like assignments export default class CallExpression extends NodeBase implements DeoptimizableEntity { arguments!: (ExpressionNode | SpreadElement)[]; callee!: ExpressionNode | Super; diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 4f34a768352..f8cc1452faa 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -79,7 +79,7 @@ export default class Identifier extends NodeBase implements PatternNode { } deoptimizePath(path: ObjectPath) { - if (!this.bound) this.bind(); + this.bind(); if (path.length === 0 && !this.scope.contains(this.name)) { this.disallowImportReassignment(); } @@ -91,7 +91,7 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - if (!this.bound) this.bind(); + this.bind(); return this.variable!.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -100,7 +100,7 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - if (!this.bound) this.bind(); + this.bind(); return this.variable!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); } diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 474c6b8fc90..8f899ff3e1f 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -19,6 +19,7 @@ import { import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; import Variable from '../variables/Variable'; +import AssignmentExpression from './AssignmentExpression'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; @@ -168,11 +169,13 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) .propertyReadSideEffects; return ( - propertyReadSideEffects === 'always' || this.property.hasEffects(context) || this.object.hasEffects(context) || - (propertyReadSideEffects && - this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context)) + // Assignments only access the object before assigning + (!(this.parent instanceof AssignmentExpression) && + propertyReadSideEffects && + (propertyReadSideEffects === 'always' || + this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context))) ); } diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 1c1f7de26a4..ebcb873c73a 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -46,6 +46,14 @@ export default class PropertyDefinition extends NodeBase { ); } + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return !this.value || this.value.hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { + return !this.value || this.value.hasEffectsWhenAssignedAtPath(path, context); + } + hasEffectsWhenCalledAtPath( path: ObjectPath, callOptions: CallOptions, diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 65c3213466f..f94a0fdb24f 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -116,10 +116,12 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { return this.objectEntity; } const staticProperties: ObjectProperty[] = []; - const dynamicProperties: ObjectProperty[] = []; + const dynamicMethods: ObjectProperty[] = []; for (const definition of this.body.body) { - const properties = definition.static ? staticProperties : dynamicProperties; + const properties = definition.static ? staticProperties : dynamicMethods; const definitionKind = (definition as MethodDefinition | { kind: undefined }).kind; + // Note that class fields do not end up on the prototype + if (properties === dynamicMethods && !definitionKind) continue; const kind = definitionKind === 'set' || definitionKind === 'get' ? definitionKind : 'init'; let key: string; if (definition.computed) { @@ -146,7 +148,7 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { key: 'prototype', kind: 'init', property: new ObjectEntity( - dynamicProperties, + dynamicMethods, this.superClass ? new ObjectMember(this.superClass, 'prototype') : OBJECT_PROTOTYPE ) }); diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 15d2fac56d2..a028afb04b0 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -82,6 +82,8 @@ export class ObjectEntity implements ExpressionEntity { : this.allProperties) { property.deoptimizePath(subPath); } + // TODO Lukas only if we have no hit here, we need to continue with the prototype + this.prototypeExpression?.deoptimizePath(path.length === 1 ? [UnknownKey, UnknownKey] : path); } getLiteralValueAtPath( @@ -136,15 +138,20 @@ export class ObjectEntity implements ExpressionEntity { hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const [key, ...subPath] = path; if (path.length > 1) { - // TODO Lukas we can look at the prototype as well, but only if the property is known? + if (typeof key !== 'string') { + return true; + } const expressionAtPath = this.getMemberExpression(key); if (expressionAtPath) { return expressionAtPath.hasEffectsWhenAccessedAtPath(subPath, context); } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } return true; } - // TODO Lukas we could match all getters for unknown paths as well as well + // TODO Lukas we could match all getters for unknown paths as well if (typeof key !== 'string' || this.hasUnknownDeoptimizedProperty) return true; const properties = this.gettersByKey[key] || this.unmatchableGetters; @@ -160,10 +167,16 @@ export class ObjectEntity implements ExpressionEntity { hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { const [key, ...subPath] = path; if (path.length > 1) { + if (typeof key !== 'string') { + return true; + } const expressionAtPath = this.getMemberExpression(key); if (expressionAtPath) { return expressionAtPath.hasEffectsWhenAssignedAtPath(subPath, context); } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); + } return true; } diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index eebe2eaae92..d8214559722 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -1,14 +1,12 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { HasEffectsContext } from '../../ExecutionContext'; import { LiteralValueOrUnknown } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; -import { ExpressionNode, IncludeChildren } from './Node'; export class ObjectMember implements ExpressionEntity { - included = false; + included = true; constructor(private readonly object: ExpressionEntity, private readonly key: string) {} @@ -53,16 +51,9 @@ export class ObjectMember implements ExpressionEntity { return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void { - this.included = true; - this.object.include(context, includeChildrenRecursively); - } + include(): void {} - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } + includeCallArguments(): void {} mayModifyThisWhenCalledAtPath( path: ObjectPath, diff --git a/test/form/samples/side-effects-class-getters-setters/_expected.js b/test/form/samples/side-effects-class-getters-setters/_expected.js index 25f4009032b..c09724d4c6a 100644 --- a/test/form/samples/side-effects-class-getters-setters/_expected.js +++ b/test/form/samples/side-effects-class-getters-setters/_expected.js @@ -23,6 +23,7 @@ class RetainedSuper { } class RetainedSub extends RetainedSuper {} RetainedSub.a; +log(); class DeoptProto { a = true; diff --git a/test/form/samples/side-effects-class-getters-setters/main.js b/test/form/samples/side-effects-class-getters-setters/main.js index 971b7bb6676..9a8c95b93cb 100644 --- a/test/form/samples/side-effects-class-getters-setters/main.js +++ b/test/form/samples/side-effects-class-getters-setters/main.js @@ -59,6 +59,7 @@ RetainedSub.a; class RemovedSub extends RetainedSuper {} RemovedSub.b; +// class fields are not part of the prototype class RemovedProtoValue { a = true; } diff --git a/test/form/samples/super-classes/super-class-prototype-access/_config.js b/test/form/samples/super-classes/super-class-prototype-access/_config.js new file mode 100644 index 00000000000..6bac45041bb --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when accessing properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-access/_expected.js b/test/form/samples/super-classes/super-class-prototype-access/_expected.js new file mode 100644 index 00000000000..936fab7c5a4 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/_expected.js @@ -0,0 +1,8 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-access/main.js b/test/form/samples/super-classes/super-class-prototype-access/main.js new file mode 100644 index 00000000000..fcf38d4ead6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-access/main.js @@ -0,0 +1,11 @@ +class SuperAccess { + prop = {}; + method() {} +} +class Access extends SuperAccess {} + +Access.prototype.doesNoExist; +Access.prototype.doesNoExist.throws; +Access.prototype.method.doesNoExist; +Access.prototype.method.doesNoExist.throws; +Access.prototype.prop.throws; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_config.js b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js new file mode 100644 index 00000000000..dd974952e7f --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when assigning properites' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js new file mode 100644 index 00000000000..840f9a4d6c1 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/_expected.js @@ -0,0 +1,31 @@ +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-assignment/main.js b/test/form/samples/super-classes/super-class-prototype-assignment/main.js new file mode 100644 index 00000000000..d47b557daf5 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-assignment/main.js @@ -0,0 +1,40 @@ +class SuperRemovedAssign { + method() {} + set prop(v) {} +} +class RemovedAssign extends SuperRemovedAssign {} +RemovedAssign.prototype.doesNotExist = 1; +RemovedAssign.prototype.method.doesNotExist = 1; +RemovedAssign.prototype.prop = 1; + +class SuperUsedAssign { + method() {} +} +class UsedAssign extends SuperUsedAssign {} +UsedAssign.prototype.doesNotExist = 1; +UsedAssign.prototype.method.doesNotExist = 1; +console.log(UsedAssign); + +class SuperAssign1 {} +class Assign1 extends SuperAssign1 {} +Assign1.prototype.doesNotExist.throws = 1; + +class SuperAssign2 { + prop = {}; +} +class Assign2 extends SuperAssign2 {} +Assign2.prototype.prop.throws = 1; + +class SuperAssign3 { + method() {} +} +class Assign3 extends SuperAssign3 {} +Assign3.prototype.method.doesNotExist.throws = 1; + +class SuperAssign4 { + set prop(v) { + console.log('effect', v); + } +} +class Assign4 extends SuperAssign4 {} +Assign4.prototype.prop = 1; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_config.js b/test/form/samples/super-classes/super-class-prototype-calls/_config.js new file mode 100644 index 00000000000..f0d4045a4f9 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when calling properties' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-calls/_expected.js b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js new file mode 100644 index 00000000000..478810e2b22 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/_expected.js @@ -0,0 +1,23 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +console.log('retained'); +console.log('retained'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-calls/main.js b/test/form/samples/super-classes/super-class-prototype-calls/main.js new file mode 100644 index 00000000000..43c62658ce6 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-calls/main.js @@ -0,0 +1,25 @@ +class SuperValues { + get prop() { + return { + effect(used) { + console.log('effect', used); + }, + isTrue() { + return true; + } + }; + } + effect(used) { + console.log('effect', used); + } + isTrue() { + return true; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue()) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue()) console.log('retained'); +else console.log('removed'); +Values.prototype.effect(); +Values.prototype.prop.effect(); diff --git a/test/form/samples/super-classes/super-class-prototype-values/_config.js b/test/form/samples/super-classes/super-class-prototype-values/_config.js new file mode 100644 index 00000000000..0f3f274db77 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'correctly resolves the prototype of the super class when reading property values' +}; diff --git a/test/form/samples/super-classes/super-class-prototype-values/_expected.js b/test/form/samples/super-classes/super-class-prototype-values/_expected.js new file mode 100644 index 00000000000..09f0c0d87ce --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/_expected.js @@ -0,0 +1,15 @@ +console.log('retained'); +console.log('retained'); +// Note that isTrueProp is not part of the prototype +console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/super-classes/super-class-prototype-values/main.js b/test/form/samples/super-classes/super-class-prototype-values/main.js new file mode 100644 index 00000000000..0fb92a2f737 --- /dev/null +++ b/test/form/samples/super-classes/super-class-prototype-values/main.js @@ -0,0 +1,28 @@ +class SuperValues { + isTrueProp = true; + get isTrue() { + return true; + } + get prop() { + return { isTrue: true }; + } +} +class Values extends SuperValues {} +if (Values.prototype.isTrue) console.log('retained'); +else console.log('removed'); +if (Values.prototype.prop.isTrue) console.log('retained'); +else console.log('removed'); +// Note that isTrueProp is not part of the prototype +if (Values.prototype.isTrueProp) console.log('removed'); +else console.log('retained'); + +const prop = { isTrue: true }; +class SuperDeopt { + get prop() { + return prop; + } +} +class Deopt extends SuperDeopt {} +Deopt.prototype.prop.isTrue = false; +if (Deopt.prototype.prop.isTrue) console.log('unimportant'); +else console.log('retained'); From 8f498f2da55c420f609912437e9d26e4b40491fc Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 25 Apr 2021 07:22:44 +0200 Subject: [PATCH 26/50] Assume created instance getters have side-effects --- src/ast/nodes/NewExpression.ts | 2 +- src/ast/nodes/shared/FunctionNode.ts | 1 + test/form/samples/nested-member-access/main.js | 3 --- .../samples/access-instance-prop/_config.js | 3 +++ test/function/samples/access-instance-prop/main.js | 13 +++++++++++++ 5 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 test/function/samples/access-instance-prop/_config.js create mode 100644 test/function/samples/access-instance-prop/main.js diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 79633447566..2685f57ad97 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -36,7 +36,7 @@ export default class NewExpression extends NodeBase { } hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + return path.length > 0; } initialise() { diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 0daab867b23..ce09283c22b 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -11,6 +11,7 @@ import SpreadElement from '../SpreadElement'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; import { PatternNode } from './Pattern'; +// TODO Lukas improve prototype handling to fix #2219 export default class FunctionNode extends NodeBase { async!: boolean; body!: BlockStatement; diff --git a/test/form/samples/nested-member-access/main.js b/test/form/samples/nested-member-access/main.js index 63a3efcc821..6a46a73687e 100644 --- a/test/form/samples/nested-member-access/main.js +++ b/test/form/samples/nested-member-access/main.js @@ -4,9 +4,6 @@ const removedResult1 = removed1.foo; const removed2 = { foo: {} }; const removedResult2 = removed2.foo.bar; -const removed3 = new function () {}(); -const removedResult3 = removed3.foo; - const removed4 = !{}; const removedResult4 = removed4.foo; diff --git a/test/function/samples/access-instance-prop/_config.js b/test/function/samples/access-instance-prop/_config.js new file mode 100644 index 00000000000..98b5968822b --- /dev/null +++ b/test/function/samples/access-instance-prop/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'respects getters when accessing properties of an instance' +}; diff --git a/test/function/samples/access-instance-prop/main.js b/test/function/samples/access-instance-prop/main.js new file mode 100644 index 00000000000..69b8f8f7946 --- /dev/null +++ b/test/function/samples/access-instance-prop/main.js @@ -0,0 +1,13 @@ +let effect = false; + +var b = { + get a() { + effect = true; + } +}; + +function X() {} +X.prototype = b; +new X().a; + +assert.ok(effect); From ef0929fa4dad6171d8294da547dcc645c1a9e377 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 25 Apr 2021 16:21:13 +0200 Subject: [PATCH 27/50] Base all expressions on a base class --- src/Chunk.ts | 2 +- src/ast/nodes/ArrayExpression.ts | 2 +- src/ast/nodes/ArrayPattern.ts | 2 +- src/ast/nodes/ArrowFunctionExpression.ts | 2 +- src/ast/nodes/BinaryExpression.ts | 2 +- src/ast/nodes/BlockStatement.ts | 2 +- src/ast/nodes/CallExpression.ts | 3 +- src/ast/nodes/CatchClause.ts | 2 +- src/ast/nodes/ConditionalExpression.ts | 3 +- src/ast/nodes/Identifier.ts | 3 +- src/ast/nodes/IfStatement.ts | 2 +- src/ast/nodes/Literal.ts | 2 +- src/ast/nodes/LogicalExpression.ts | 3 +- src/ast/nodes/MemberExpression.ts | 2 +- src/ast/nodes/ObjectExpression.ts | 3 +- src/ast/nodes/Property.ts | 3 +- src/ast/nodes/PropertyDefinition.ts | 3 +- src/ast/nodes/RestElement.ts | 3 +- src/ast/nodes/ReturnStatement.ts | 2 +- src/ast/nodes/SequenceExpression.ts | 2 +- src/ast/nodes/TemplateLiteral.ts | 2 +- src/ast/nodes/UnaryExpression.ts | 2 +- src/ast/nodes/VariableDeclarator.ts | 2 +- src/ast/nodes/shared/ClassNode.ts | 3 +- src/ast/nodes/shared/Expression.ts | 78 +++++++++++++++++------- src/ast/nodes/shared/FunctionNode.ts | 8 +-- src/ast/nodes/shared/MethodBase.ts | 16 +++-- src/ast/nodes/shared/MethodTypes.ts | 23 +++---- src/ast/nodes/shared/MultiExpression.ts | 25 +------- src/ast/nodes/shared/Node.ts | 57 +---------------- src/ast/nodes/shared/ObjectEntity.ts | 27 ++++---- src/ast/nodes/shared/ObjectMember.ts | 15 ++--- src/ast/scopes/BlockScope.ts | 3 +- src/ast/scopes/ModuleScope.ts | 2 +- src/ast/scopes/ParameterScope.ts | 2 +- src/ast/scopes/ReturnValueScope.ts | 3 +- src/ast/scopes/Scope.ts | 2 +- src/ast/unknownValues.ts | 61 ------------------ src/ast/values.ts | 34 +++++++---- src/ast/variables/ArgumentsVariable.ts | 2 +- src/ast/variables/LocalVariable.ts | 3 +- src/ast/variables/ThisVariable.ts | 3 +- src/ast/variables/UndefinedVariable.ts | 2 +- src/ast/variables/Variable.ts | 61 ++---------------- 44 files changed, 162 insertions(+), 322 deletions(-) delete mode 100644 src/ast/unknownValues.ts diff --git a/src/Chunk.ts b/src/Chunk.ts index 9f583ea4a3e..0d79ffb1811 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -3,7 +3,7 @@ import { relative } from '../browser/path'; import ExportDefaultDeclaration from './ast/nodes/ExportDefaultDeclaration'; import FunctionDeclaration from './ast/nodes/FunctionDeclaration'; import ChildScope from './ast/scopes/ChildScope'; -import { UNDEFINED_EXPRESSION } from './ast/unknownValues'; +import { UNDEFINED_EXPRESSION } from './ast/values'; import ExportDefaultVariable from './ast/variables/ExportDefaultVariable'; import ExportShimVariable from './ast/variables/ExportShimVariable'; import LocalVariable from './ast/variables/LocalVariable'; diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index dcc9ffdf403..d8f95b49fe4 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,6 +1,5 @@ import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import { arrayMembers, @@ -8,6 +7,7 @@ import { hasMemberEffectWhenCalled, } from '../values'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/ArrayPattern.ts b/src/ast/nodes/ArrayPattern.ts index ed21bf3bd28..e0d23d61be4 100644 --- a/src/ast/nodes/ArrayPattern.ts +++ b/src/ast/nodes/ArrayPattern.ts @@ -1,8 +1,8 @@ import { HasEffectsContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 1664fd73a9b..7673a5379b6 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -2,12 +2,12 @@ import { CallOptions } from '../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import ReturnValueScope from '../scopes/ReturnValueScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../utils/PathTracker'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import RestElement from './RestElement'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/BinaryExpression.ts b/src/ast/nodes/BinaryExpression.ts index 3a26ebaba7c..26f632b307d 100644 --- a/src/ast/nodes/BinaryExpression.ts +++ b/src/ast/nodes/BinaryExpression.ts @@ -1,6 +1,5 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -10,6 +9,7 @@ import { import ExpressionStatement from './ExpressionStatement'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const binaryOperators: { diff --git a/src/ast/nodes/BlockStatement.ts b/src/ast/nodes/BlockStatement.ts index c9c4166f950..614d5aa2de5 100644 --- a/src/ast/nodes/BlockStatement.ts +++ b/src/ast/nodes/BlockStatement.ts @@ -4,9 +4,9 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import BlockScope from '../scopes/BlockScope'; import ChildScope from '../scopes/ChildScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import ExpressionStatement from './ExpressionStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { IncludeChildren, Node, StatementBase, StatementNode } from './shared/Node'; export default class BlockStatement extends StatementBase { diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 3171d8c4ba4..3afb1f4b9d7 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,7 +9,6 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -20,7 +19,7 @@ import { import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { Annotation, ExpressionNode, diff --git a/src/ast/nodes/CatchClause.ts b/src/ast/nodes/CatchClause.ts index 41acfb00cee..77e8eb2eca6 100644 --- a/src/ast/nodes/CatchClause.ts +++ b/src/ast/nodes/CatchClause.ts @@ -1,8 +1,8 @@ import CatchScope from '../scopes/CatchScope'; import Scope from '../scopes/Scope'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import BlockStatement from './BlockStatement'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 46b5bcb1bc8..fe440cd59a8 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,7 +11,6 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -21,7 +20,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index f8cc1452faa..874764e5196 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -7,13 +7,12 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import FunctionScope from '../scopes/FunctionScope'; -import { LiteralValueOrUnknown } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/IfStatement.ts b/src/ast/nodes/IfStatement.ts index 1e2fa374b70..4231e1382d3 100644 --- a/src/ast/nodes/IfStatement.ts +++ b/src/ast/nodes/IfStatement.ts @@ -4,11 +4,11 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../ExecutionContext'; import TrackingScope from '../scopes/TrackingScope'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import BlockStatement from './BlockStatement'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, GenericEsTreeNode, diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index 964d05e9c43..c207a09318b 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -1,7 +1,6 @@ import MagicString from 'magic-string'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; import { getLiteralMembersForValue, @@ -10,6 +9,7 @@ import { MemberDescription, } from '../values'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { GenericEsTreeNode, NodeBase } from './shared/Node'; export type LiteralValue = string | boolean | null | number | RegExp | undefined; diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index b6d89366d91..f6dfe076c21 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,7 +11,6 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -21,7 +20,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 8f899ff3e1f..2cc18f94866 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -6,7 +6,6 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -24,6 +23,7 @@ import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index d69357aa920..541c4f0f967 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,7 +4,6 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -16,7 +15,7 @@ import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import Property from './Property'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 8222e0dffc4..40015a7dc02 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -2,10 +2,9 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; import { HasEffectsContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import MethodBase from './shared/MethodBase'; import { ExpressionNode } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index ebcb873c73a..af731437408 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,11 +1,10 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 1b06d71be30..b6d804dc118 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,9 +1,8 @@ import { HasEffectsContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity } from './shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; diff --git a/src/ast/nodes/ReturnStatement.ts b/src/ast/nodes/ReturnStatement.ts index a5bed281411..37b42812bd8 100644 --- a/src/ast/nodes/ReturnStatement.ts +++ b/src/ast/nodes/ReturnStatement.ts @@ -5,8 +5,8 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import * as NodeType from './NodeType'; +import { UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, StatementBase } from './shared/Node'; export default class ReturnStatement extends StatementBase { diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index caf8a694176..ed06447e7cb 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -10,10 +10,10 @@ import { treeshakeNode } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown } from '../unknownValues'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { diff --git a/src/ast/nodes/TemplateLiteral.ts b/src/ast/nodes/TemplateLiteral.ts index 198345f2785..06314a3bc00 100644 --- a/src/ast/nodes/TemplateLiteral.ts +++ b/src/ast/nodes/TemplateLiteral.ts @@ -1,8 +1,8 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import TemplateElement from './TemplateElement'; diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index df261153451..f96fdc44a0c 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,10 +1,10 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../unknownValues'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; +import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; const unaryOperators: { diff --git a/src/ast/nodes/VariableDeclarator.ts b/src/ast/nodes/VariableDeclarator.ts index 333ebbfd860..40cd81a57db 100644 --- a/src/ast/nodes/VariableDeclarator.ts +++ b/src/ast/nodes/VariableDeclarator.ts @@ -7,8 +7,8 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { UNDEFINED_EXPRESSION } from '../unknownValues'; import { ObjectPath } from '../utils/PathTracker'; +import { UNDEFINED_EXPRESSION } from '../values'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index f94a0fdb24f..21bf4ca4071 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -3,7 +3,6 @@ import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; -import { LiteralValueOrUnknown, UnknownValue } from '../../unknownValues'; import { EMPTY_PATH, ObjectPath, @@ -15,7 +14,7 @@ import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; -import { ExpressionEntity } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { ObjectEntity, ObjectProperty } from './ObjectEntity'; import { ObjectMember } from './ObjectMember'; diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 48614f18c30..9047cfcdd8e 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,13 +2,19 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { LiteralValue } from '../Literal'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; -export interface ExpressionEntity extends WritableEntity { - included: boolean; +export const UnknownValue = Symbol('Unknown Value'); + +export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; + +export class ExpressionEntity implements WritableEntity { + included = false; + + deoptimizePath(_path: ObjectPath): void {} /** * If possible it returns a stringifyable literal value for this node that can be used @@ -16,26 +22,54 @@ export interface ExpressionEntity extends WritableEntity { * Otherwise it should return UnknownValue. */ getLiteralValueAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): LiteralValueOrUnknown; + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): LiteralValueOrUnknown { + return UnknownValue; + } + getReturnExpressionWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): ExpressionEntity; - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean; + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): ExpressionEntity { + return UNKNOWN_EXPRESSION; + } + + hasEffectsWhenAccessedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { + return true; + } + + hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext): boolean { + return true; + } + hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ): boolean; - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren): void; - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void; + _path: ObjectPath, + _callOptions: CallOptions, + _context: HasEffectsContext + ): boolean { + return true; + } + + include(_context: InclusionContext, _includeChildrenRecursively: IncludeChildren): void { + this.included = true; + } + + includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { + for (const arg of args) { + arg.include(context, false); + } + } + mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): boolean; + _path: ObjectPath, + _recursionTracker: PathTracker, + _origin: DeoptimizableEntity + ): boolean { + return true; + } } + +export const UNKNOWN_EXPRESSION: ExpressionEntity = new (class UnknownExpression extends ExpressionEntity {})(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index ce09283c22b..94275c72dba 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,13 +1,13 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import FunctionScope from '../../scopes/FunctionScope'; -import { UNKNOWN_EXPRESSION } from '../../unknownValues'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; import { UnknownObjectExpression } from '../../values'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; +import { UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; import { PatternNode } from './Pattern'; @@ -123,10 +123,8 @@ export default class FunctionNode extends NodeBase { this.body.addImplicitReturnExpressionToScope(); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath - ) { - return path.length ? true : this.referencesThis + mayModifyThisWhenCalledAtPath(path: ObjectPath) { + return path.length ? true : this.referencesThis; } parseNode(esTreeNode: GenericEsTreeNode) { diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 1d48d969195..094decea27a 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -1,10 +1,14 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from '../../unknownValues'; -import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER +} from '../../utils/PathTracker'; import PrivateIdentifier from '../PrivateIdentifier'; -import { ExpressionEntity } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { PatternNode } from './Pattern'; @@ -86,9 +90,11 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity if (this.accessedValue === null) { if (this.kind === 'get') { this.accessedValue = UNKNOWN_EXPRESSION; - return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath(EMPTY_PATH, + return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath( + EMPTY_PATH, SHARED_RECURSION_TRACKER, - this)); + this + )); } else { return (this.accessedValue = this.value); } diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index d0c7c80af0a..1c6d142de05 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,10 +1,9 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; import { EMPTY_PATH, ObjectPath } from '../../utils/PathTracker'; import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_STRING } from '../../values'; import SpreadElement from '../SpreadElement'; -import { ExpressionEntity } from './Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode } from './Node'; type MethodDescription = { @@ -21,15 +20,9 @@ type MethodDescription = { } ); -class Method implements ExpressionEntity { - public included = true; - - constructor(private readonly description: MethodDescription) {} - - deoptimizePath(): void {} - - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; +class Method extends ExpressionEntity { + constructor(private readonly description: MethodDescription) { + super(); } getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity { @@ -75,8 +68,6 @@ class Method implements ExpressionEntity { return false; } - include(): void {} - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { for (const arg of args) { arg.include(context, false); @@ -93,18 +84,18 @@ export const METHOD_RETURNS_BOOLEAN = new Method({ mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN -}) +}); export const METHOD_RETURNS_STRING = new Method({ callsArgs: null, mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_STRING -}) +}); export const METHOD_RETURNS_UNKNOWN = new Method({ callsArgs: null, mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_EXPRESSION -}) +}); diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index 0a6943f9d7d..f5562a30c30 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -1,18 +1,15 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; import { ExpressionEntity } from './Expression'; import { IncludeChildren } from './Node'; -export class MultiExpression implements ExpressionEntity { +export class MultiExpression extends ExpressionEntity { included = false; - private expressions: ExpressionEntity[]; - - constructor(expressions: ExpressionEntity[]) { - this.expressions = expressions; + constructor(private expressions: ExpressionEntity[]) { + super(); } deoptimizePath(path: ObjectPath): void { @@ -21,10 +18,6 @@ export class MultiExpression implements ExpressionEntity { } } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - getReturnExpressionWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -71,16 +64,4 @@ export class MultiExpression implements ExpressionEntity { } } } - - includeCallArguments(): void {} - - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - return this.expressions.some(e => - e.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) - ); - } } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 71323ab7c78..5f8ba91ef01 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -3,8 +3,6 @@ import { locate } from 'locate-character'; import MagicString from 'magic-string'; import { AstContext } from '../../../Module'; import { NodeRenderOptions, RenderOptions } from '../../../utils/renderHelpers'; -import { CallOptions } from '../../CallOptions'; -import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { Entity } from '../../Entity'; import { createHasEffectsContext, @@ -13,11 +11,8 @@ import { } from '../../ExecutionContext'; import { getAndCreateKeys, keys } from '../../keys'; import ChildScope from '../../scopes/ChildScope'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; import Variable from '../../variables/Variable'; import * as NodeType from '../NodeType'; -import SpreadElement from '../SpreadElement'; import { ExpressionEntity } from './Expression'; export interface GenericEsTreeNode extends acorn.Node { @@ -91,12 +86,11 @@ export interface StatementNode extends Node {} export interface ExpressionNode extends ExpressionEntity, Node {} -export class NodeBase implements ExpressionNode { +export class NodeBase extends ExpressionEntity implements ExpressionNode { annotations?: Annotation[]; context: AstContext; end!: number; esTreeNode: acorn.Node; - included = false; keys: string[]; parent: Node | { context: AstContext; type: string }; scope!: ChildScope; @@ -108,6 +102,7 @@ export class NodeBase implements ExpressionNode { parent: Node | { context: AstContext; type: string }, parentScope: ChildScope ) { + super(); this.esTreeNode = esTreeNode; this.keys = keys[esTreeNode.type] || getAndCreateKeys(esTreeNode); this.parent = parent; @@ -149,24 +144,6 @@ export class NodeBase implements ExpressionNode { this.scope = parentScope; } - deoptimizePath(_path: ObjectPath) {} - - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffects(context: HasEffectsContext): boolean { for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; @@ -180,22 +157,6 @@ export class NodeBase implements ExpressionNode { return false; } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { - return path.length > 0; - } - - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; for (const key of this.keys) { @@ -215,12 +176,6 @@ export class NodeBase implements ExpressionNode { this.include(context, includeChildrenRecursively); } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - /** * Override to perform special initialisation steps after the scope is initialised */ @@ -232,14 +187,6 @@ export class NodeBase implements ExpressionNode { } } - mayModifyThisWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ) { - return true; - } - parseNode(esTreeNode: GenericEsTreeNode) { for (const key of Object.keys(esTreeNode)) { // That way, we can override this function to add custom initialisation and then call super.parseNode diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index a028afb04b0..e57576d20a6 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,9 +1,19 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../../unknownValues'; -import { ObjectPath, ObjectPathKey, PathTracker, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { ExpressionEntity } from './Expression'; +import { + ObjectPath, + ObjectPathKey, + PathTracker, + UnknownKey, + UNKNOWN_PATH +} from '../../utils/PathTracker'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './Expression'; export interface ObjectProperty { key: ObjectPathKey; @@ -14,11 +24,9 @@ export interface ObjectProperty { type PropertyMap = Record; // TODO Lukas add a way to directly inject only propertiesByKey and create allProperties lazily/not -export class ObjectEntity implements ExpressionEntity { - included = true; - +export class ObjectEntity extends ExpressionEntity { private readonly allProperties: ExpressionEntity[] = []; - private readonly deoptimizedPaths: Record = Object.create(null); + private readonly deoptimizedPaths: Record = Object.create(null); private readonly expressionsToBeDeoptimizedByKey: Record< string, DeoptimizableEntity[] @@ -32,6 +40,7 @@ export class ObjectEntity implements ExpressionEntity { private readonly unmatchableSetters: ExpressionEntity[] = []; constructor(properties: ObjectProperty[], private prototypeExpression: ExpressionEntity | null) { + super(); this.buildPropertyMaps(properties); } @@ -208,10 +217,6 @@ export class ObjectEntity implements ExpressionEntity { return true; } - include() {} - - includeCallArguments() {} - mayModifyThisWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index d8214559722..bcdd1570961 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -1,14 +1,13 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { LiteralValueOrUnknown } from '../../unknownValues'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { ExpressionEntity } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; -export class ObjectMember implements ExpressionEntity { - included = true; - - constructor(private readonly object: ExpressionEntity, private readonly key: string) {} +export class ObjectMember extends ExpressionEntity { + constructor(private readonly object: ExpressionEntity, private readonly key: string) { + super(); + } deoptimizePath(path: ObjectPath): void { this.object.deoptimizePath([this.key, ...path]); @@ -51,10 +50,6 @@ export class ObjectMember implements ExpressionEntity { return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); } - include(): void {} - - includeCallArguments(): void {} - mayModifyThisWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/scopes/BlockScope.ts b/src/ast/scopes/BlockScope.ts index 847263dfb2d..d6393239f6e 100644 --- a/src/ast/scopes/BlockScope.ts +++ b/src/ast/scopes/BlockScope.ts @@ -1,7 +1,6 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; diff --git a/src/ast/scopes/ModuleScope.ts b/src/ast/scopes/ModuleScope.ts index 816d77e1670..372839a7dd0 100644 --- a/src/ast/scopes/ModuleScope.ts +++ b/src/ast/scopes/ModuleScope.ts @@ -1,7 +1,7 @@ import { AstContext } from '../../Module'; import { InternalModuleFormat } from '../../rollup/types'; import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; -import { UNDEFINED_EXPRESSION } from '../unknownValues'; +import { UNDEFINED_EXPRESSION } from '../values'; import ExportDefaultVariable from '../variables/ExportDefaultVariable'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; diff --git a/src/ast/scopes/ParameterScope.ts b/src/ast/scopes/ParameterScope.ts index d24075f8781..77d30c09d7f 100644 --- a/src/ast/scopes/ParameterScope.ts +++ b/src/ast/scopes/ParameterScope.ts @@ -1,9 +1,9 @@ import { AstContext } from '../../Module'; import { InclusionContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ExpressionNode } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; import LocalVariable from '../variables/LocalVariable'; import ChildScope from './ChildScope'; import Scope from './Scope'; diff --git a/src/ast/scopes/ReturnValueScope.ts b/src/ast/scopes/ReturnValueScope.ts index f276a5978b4..3045740825a 100644 --- a/src/ast/scopes/ReturnValueScope.ts +++ b/src/ast/scopes/ReturnValueScope.ts @@ -1,5 +1,4 @@ -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { UNKNOWN_PATH } from '../utils/PathTracker'; import ParameterScope from './ParameterScope'; diff --git a/src/ast/scopes/Scope.ts b/src/ast/scopes/Scope.ts index 4f99aa86f42..32a46d31719 100644 --- a/src/ast/scopes/Scope.ts +++ b/src/ast/scopes/Scope.ts @@ -1,7 +1,7 @@ import { AstContext } from '../../Module'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { UNDEFINED_EXPRESSION } from '../unknownValues'; +import { UNDEFINED_EXPRESSION } from '../values'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import ChildScope from './ChildScope'; diff --git a/src/ast/unknownValues.ts b/src/ast/unknownValues.ts deleted file mode 100644 index 002cb4cb51c..00000000000 --- a/src/ast/unknownValues.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { CallOptions } from './CallOptions'; -import { HasEffectsContext, InclusionContext } from './ExecutionContext'; -import { LiteralValue } from './nodes/Literal'; -import { ExpressionEntity } from './nodes/shared/Expression'; -import { ExpressionNode } from './nodes/shared/Node'; -import SpreadElement from './nodes/SpreadElement'; -import { ObjectPath } from './utils/PathTracker'; - -export const UnknownValue = Symbol('Unknown Value'); -export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; - -export class UnknownExpression implements ExpressionEntity { - included = true; - - deoptimizePath() {} - - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - - getReturnExpressionWhenCalledAtPath(_path: ObjectPath) { - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { - return path.length > 0; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 0; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - - include() {} - - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]) { - for (const arg of args) { - arg.include(context, false); - } - } - - mayModifyThisWhenCalledAtPath() { - return true; - } -} - -export const UNKNOWN_EXPRESSION: ExpressionEntity = new UnknownExpression(); - -// TODO Lukas maybe move this back to values -export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpression extends UnknownExpression { - getLiteralValueAtPath() { - return undefined; - } -})(); diff --git a/src/ast/values.ts b/src/ast/values.ts index 77bbe6aeaa1..bc3228a5484 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -1,8 +1,7 @@ import { CallOptions, NO_ARGS } from './CallOptions'; import { HasEffectsContext } from './ExecutionContext'; import { LiteralValue } from './nodes/Literal'; -import { ExpressionEntity } from './nodes/shared/Expression'; -import { UnknownExpression, UNKNOWN_EXPRESSION } from './unknownValues'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './nodes/shared/Expression'; import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { @@ -27,6 +26,11 @@ function assembleMemberDescriptions( return Object.create(inheritedDescriptions, memberDescriptions); } +export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpression extends ExpressionEntity { + getLiteralValueAtPath() { + return undefined; + } +})(); const returnsUnknown: RawMemberDescription = { value: { @@ -43,7 +47,7 @@ const callsArgReturnsUnknown: RawMemberDescription = { value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: [0], mutatesSelf: false } }; -export class UnknownArrayExpression extends UnknownExpression { +export class UnknownArrayExpression extends ExpressionEntity { included = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { @@ -110,7 +114,7 @@ const callsArgMutatesSelfReturnsArray: RawMemberDescription = { } }; -export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolean extends UnknownExpression { +export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoolean extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalBooleanMembers, path[0]); @@ -119,7 +123,7 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolea } hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1 + return path.length > 1; } hasEffectsWhenCalledAtPath(path: ObjectPath) { @@ -129,7 +133,7 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new class UnknownBoolea } return true; } -}; +})(); const returnsBoolean: RawMemberDescription = { value: { @@ -148,7 +152,7 @@ const callsArgReturnsBoolean: RawMemberDescription = { } }; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends UnknownExpression { +const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); @@ -156,7 +160,9 @@ const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { + return path.length > 1; + } hasEffectsWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { @@ -165,7 +171,7 @@ const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new class UnknownNumber extends } return true; } -}; +})(); const returnsNumber: RawMemberDescription = { value: { @@ -192,7 +198,7 @@ const callsArgReturnsNumber: RawMemberDescription = { } }; -export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString extends UnknownExpression { +export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalStringMembers, path[0]); @@ -200,7 +206,9 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString return UNKNOWN_EXPRESSION; } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1 } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { + return path.length > 1; + } hasEffectsWhenCalledAtPath( path: ObjectPath, @@ -212,7 +220,7 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new class UnknownString } return true; } -}; +})(); const returnsString: RawMemberDescription = { value: { @@ -226,7 +234,7 @@ const returnsString: RawMemberDescription = { // TODO Lukas This could just be the constant and above, we use an ObjectEntity with OBJECT_PROTOTYPE as prototype // TODO Lukas Also, the name should reflect we assume neither getters nor setters and do not override builtins // or just remove? -export class UnknownObjectExpression extends UnknownExpression { +export class UnknownObjectExpression extends ExpressionEntity { included = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { diff --git a/src/ast/variables/ArgumentsVariable.ts b/src/ast/variables/ArgumentsVariable.ts index 234f389b0cc..d511000e0cd 100644 --- a/src/ast/variables/ArgumentsVariable.ts +++ b/src/ast/variables/ArgumentsVariable.ts @@ -1,5 +1,5 @@ import { AstContext } from '../../Module'; -import { UNKNOWN_EXPRESSION } from '../unknownValues'; +import { UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 95e5e9dba27..268fb83d9fb 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -5,10 +5,9 @@ import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; -import { ExpressionEntity } from '../nodes/shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 93f7f00f318..3294216f6c5 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,8 +1,7 @@ import { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { ExpressionEntity } from '../nodes/shared/Expression'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; diff --git a/src/ast/variables/UndefinedVariable.ts b/src/ast/variables/UndefinedVariable.ts index 2cf9eee9643..83a8d6220df 100644 --- a/src/ast/variables/UndefinedVariable.ts +++ b/src/ast/variables/UndefinedVariable.ts @@ -1,4 +1,4 @@ -import { LiteralValueOrUnknown } from '../unknownValues'; +import { LiteralValueOrUnknown } from '../nodes/shared/Expression'; import Variable from './Variable'; export default class UndefinedVariable extends Variable { diff --git a/src/ast/variables/Variable.ts b/src/ast/variables/Variable.ts index 3deb48e22a8..fef15ed6f25 100644 --- a/src/ast/variables/Variable.ts +++ b/src/ast/variables/Variable.ts @@ -1,30 +1,23 @@ import ExternalModule from '../../ExternalModule'; import Module from '../../Module'; import { RESERVED_NAMES } from '../../utils/reservedNames'; -import { CallOptions } from '../CallOptions'; -import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import Identifier from '../nodes/Identifier'; import { ExpressionEntity } from '../nodes/shared/Expression'; -import { ExpressionNode } from '../nodes/shared/Node'; -import SpreadElement from '../nodes/SpreadElement'; -import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../unknownValues'; -import { ObjectPath, PathTracker } from '../utils/PathTracker'; +import { ObjectPath } from '../utils/PathTracker'; -export default class Variable implements ExpressionEntity { +export default class Variable extends ExpressionEntity { alwaysRendered = false; - included = false; isId = false; // both NamespaceVariable and ExternalVariable can be namespaces isNamespace?: boolean; isReassigned = false; module?: Module | ExternalModule; - name: string; renderBaseName: string | null = null; renderName: string | null = null; - constructor(name: string) { - this.name = name; + constructor(public name: string) { + super(); } /** @@ -33,20 +26,10 @@ export default class Variable implements ExpressionEntity { */ addReference(_identifier: Identifier) {} - deoptimizePath(_path: ObjectPath) {} - getBaseVariableName(): string { return this.renderBaseName || this.renderName || this.name; } - getLiteralValueAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): LiteralValueOrUnknown { - return UnknownValue; - } - getName(): string { const name = this.renderName || this.name; return this.renderBaseName @@ -54,30 +37,10 @@ export default class Variable implements ExpressionEntity { : name; } - getReturnExpressionWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): ExpressionEntity { - return UNKNOWN_EXPRESSION; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath, _context: HasEffectsContext) { return path.length > 0; } - hasEffectsWhenAssignedAtPath(_path: ObjectPath, _context: HasEffectsContext) { - return true; - } - - hasEffectsWhenCalledAtPath( - _path: ObjectPath, - _callOptions: CallOptions, - _context: HasEffectsContext - ) { - return true; - } - /** * Marks this variable as being part of the bundle, which is usually the case when one of * its identifiers becomes part of the bundle. Returns true if it has not been included @@ -88,22 +51,8 @@ export default class Variable implements ExpressionEntity { this.included = true; } - includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - for (const arg of args) { - arg.include(context, false); - } - } - markCalledFromTryStatement() {} - mayModifyThisWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ) { - return true; - } - setRenderNames(baseName: string | null, name: string | null) { this.renderBaseName = baseName; this.renderName = name; From 145d01f5644eed6376e4b620e2c14ffda9322491 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 30 Apr 2021 06:43:54 +0200 Subject: [PATCH 28/50] Only deoptimize necessary paths when deoptimizing "this" --- src/ast/nodes/ArrowFunctionExpression.ts | 7 +-- src/ast/nodes/CallExpression.ts | 21 ++++--- src/ast/nodes/Identifier.ts | 24 +++++--- src/ast/nodes/MemberExpression.ts | 47 +++++++++------ src/ast/nodes/ObjectExpression.ts | 30 +++++++--- src/ast/nodes/PropertyDefinition.ts | 25 +++++--- src/ast/nodes/ThisExpression.ts | 11 ---- src/ast/nodes/shared/ClassNode.ts | 24 +++++--- src/ast/nodes/shared/Expression.ts | 24 +++++--- src/ast/nodes/shared/FunctionNode.ts | 18 +++--- src/ast/nodes/shared/MethodBase.ts | 29 +++++++--- src/ast/nodes/shared/MethodTypes.ts | 14 +++-- src/ast/nodes/shared/ObjectEntity.ts | 57 +++++++++++-------- src/ast/nodes/shared/ObjectMember.ts | 19 ++++--- src/ast/variables/LocalVariable.ts | 51 ++++++++++------- src/ast/variables/ThisVariable.ts | 28 ++++++++- .../samples/minimal-this-mutation/_config.js | 3 + .../minimal-this-mutation/_expected.js | 48 ++++++++++++++++ .../samples/minimal-this-mutation/main.js | 56 ++++++++++++++++++ 19 files changed, 377 insertions(+), 159 deletions(-) create mode 100644 test/form/samples/minimal-this-mutation/_config.js create mode 100644 test/form/samples/minimal-this-mutation/_expected.js create mode 100644 test/form/samples/minimal-this-mutation/main.js diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 7673a5379b6..b9d1ba672f4 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -31,6 +31,9 @@ export default class ArrowFunctionExpression extends NodeBase { } } + // Arrow functions do not mutate their context + deoptimizeThisOnEventAtPath() {} + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -98,10 +101,6 @@ export default class ArrowFunctionExpression extends NodeBase { } } - mayModifyThisWhenCalledAtPath() { - return false; - } - parseNode(esTreeNode: GenericEsTreeNode) { if (esTreeNode.body.type === NodeType.BlockStatement) { this.body = new this.context.nodeConstructors.BlockStatement( diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 3afb1f4b9d7..35752d936fe 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -19,7 +19,13 @@ import { import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; +import { + EVENT_CALLED, + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { Annotation, ExpressionNode, @@ -269,12 +275,13 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt private applyDeoptimizations() { this.deoptimized = true; - if ( - this.callee instanceof MemberExpression && - !this.callee.variable && - this.callee.mayModifyThisWhenCalledAtPath([], SHARED_RECURSION_TRACKER, this) - ) { - this.callee.object.deoptimizePath(UNKNOWN_PATH); + if (this.callee instanceof MemberExpression && !this.callee.variable) { + this.callee.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + this.callee.object, + SHARED_RECURSION_TRACKER + ); } for (const argument of this.arguments) { // This will make sure all properties of parameters behave as "unknown" diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 874764e5196..c2a14105174 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -12,7 +12,7 @@ import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; @@ -85,6 +85,20 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable!.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.variable!.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -140,14 +154,6 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable!.includeCallArguments(context, args); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - return this.variable!.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - render( code: MagicString, _options: RenderOptions, diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 07205022ef4..19d7ba4c741 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -23,7 +23,12 @@ import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UnknownValue +} from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -131,6 +136,30 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + this.bind(); + if (this.variable) { + this.variable.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } else { + this.object.deoptimizeThisOnEventAtPath( + event, + [this.propertyKey!, ...path], + thisParameter, + recursionTracker + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -232,22 +261,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.propertyKey = getResolvablePropertyKey(this); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - this.bind(); - if (this.variable) { - return this.variable.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - return this.object.mayModifyThisWhenCalledAtPath( - [this.propertyKey!, ...path], - recursionTracker, - origin - ); - } - render( code: MagicString, options: RenderOptions, diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 541c4f0f967..3f149d5221b 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -15,7 +15,13 @@ import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import Property from './Property'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { NodeBase } from './shared/Node'; import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import { OBJECT_PROTOTYPE } from './shared/ObjectPrototype'; @@ -35,6 +41,20 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE this.getObjectEntity().deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -71,14 +91,6 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - return this.getObjectEntity().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - render( code: MagicString, options: RenderOptions, diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index af731437408..968fb787dbc 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -4,7 +4,13 @@ import { HasEffectsContext } from '../ExecutionContext'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UnknownValue, + UNKNOWN_EXPRESSION +} from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class PropertyDefinition extends NodeBase { @@ -18,6 +24,15 @@ export default class PropertyDefinition extends NodeBase { this.value?.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.value?.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -60,12 +75,4 @@ export default class PropertyDefinition extends NodeBase { ) { return !this.value || this.value.hasEffectsWhenCalledAtPath(path, callOptions, context); } - - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): boolean { - return this.value?.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin) || false; - } } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index 55dbef817fd..ba580fd0ce7 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -4,8 +4,6 @@ import ModuleScope from '../scopes/ModuleScope'; import { ObjectPath } from '../utils/PathTracker'; import ThisVariable from '../variables/ThisVariable'; import * as NodeType from './NodeType'; -import ClassNode from './shared/ClassNode'; -import FunctionNode from './shared/FunctionNode'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { @@ -54,15 +52,6 @@ export default class ThisExpression extends NodeBase { this.start ); } - for (let parent = this.parent; parent instanceof NodeBase; parent = parent.parent) { - if (parent instanceof ClassNode) { - break; - } - if (parent instanceof FunctionNode) { - parent.referencesThis = true; - break; - } - } } render(code: MagicString) { diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 21bf4ca4071..a17e09b6c1a 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -14,7 +14,7 @@ import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent, UnknownValue } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { ObjectEntity, ObjectProperty } from './ObjectEntity'; import { ObjectMember } from './ObjectMember'; @@ -43,6 +43,20 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { this.getObjectEntity().deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getObjectEntity().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -102,14 +116,6 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { this.classConstructor = null; } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - return this.getObjectEntity().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - private getObjectEntity(): ObjectEntity { if (this.objectEntity !== null) { return this.objectEntity; diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 9047cfcdd8e..d6711a3bfdd 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,7 +2,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { ObjectPath, PathTracker } from '../../utils/PathTracker'; +import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; import { LiteralValue } from '../Literal'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; @@ -11,11 +11,25 @@ export const UnknownValue = Symbol('Unknown Value'); export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; +export const EVENT_ACCESSED = 0; +export const EVENT_ASSIGNED = 1; +export const EVENT_CALLED = 2; +export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; + export class ExpressionEntity implements WritableEntity { included = false; deoptimizePath(_path: ObjectPath): void {} + deoptimizeThisOnEventAtPath( + _event: NodeEvent, + _path: ObjectPath, + thisParameter: ExpressionEntity, + _recursionTracker: PathTracker + ): void { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } + /** * If possible it returns a stringifyable literal value for this node that can be used * for inlining or comparing values. @@ -62,14 +76,6 @@ export class ExpressionEntity implements WritableEntity { arg.include(context, false); } } - - mayModifyThisWhenCalledAtPath( - _path: ObjectPath, - _recursionTracker: PathTracker, - _origin: DeoptimizableEntity - ): boolean { - return true; - } } export const UNKNOWN_EXPRESSION: ExpressionEntity = new (class UnknownExpression extends ExpressionEntity {})(); diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 94275c72dba..280cf569cc6 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -7,7 +7,7 @@ import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; -import { UNKNOWN_EXPRESSION } from './Expression'; +import { EVENT_CALLED, ExpressionEntity, NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; import { PatternNode } from './Pattern'; @@ -18,7 +18,6 @@ export default class FunctionNode extends NodeBase { id!: IdentifierWithVariable | null; params!: PatternNode[]; preventChildBlockScope!: true; - referencesThis!: boolean; scope!: FunctionScope; private isPrototypeDeoptimized = false; @@ -41,6 +40,16 @@ export default class FunctionNode extends NodeBase { } } + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED) { + if (path.length > 0 ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + this.scope.thisVariable.addEntityToBeDeoptimized(thisParameter); + } + } + } + getReturnExpressionWhenCalledAtPath(path: ObjectPath) { return path.length === 0 ? this.scope.getReturnExpression() : UNKNOWN_EXPRESSION; } @@ -123,12 +132,7 @@ export default class FunctionNode extends NodeBase { this.body.addImplicitReturnExpressionToScope(); } - mayModifyThisWhenCalledAtPath(path: ObjectPath) { - return path.length ? true : this.referencesThis; - } - parseNode(esTreeNode: GenericEsTreeNode) { - this.referencesThis = false; this.body = new this.context.nodeConstructors.BlockStatement( esTreeNode.body, this, diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 094decea27a..66611cb7087 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -8,7 +8,12 @@ import { SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; import PrivateIdentifier from '../PrivateIdentifier'; -import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UNKNOWN_EXPRESSION +} from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { PatternNode } from './Pattern'; @@ -32,6 +37,20 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity this.getAccessedValue().deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.getAccessedValue().deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -78,14 +97,6 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity return this.getAccessedValue().hasEffectsWhenCalledAtPath(path, callOptions, context); } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): boolean { - return this.getAccessedValue().mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - protected getAccessedValue(): ExpressionEntity { if (this.accessedValue === null) { if (this.kind === 'get') { diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index 1c6d142de05..6f684d06dc3 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,9 +1,9 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; -import { EMPTY_PATH, ObjectPath } from '../../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../../utils/PathTracker'; import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_STRING } from '../../values'; import SpreadElement from '../SpreadElement'; -import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { EVENT_CALLED, ExpressionEntity, NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode } from './Node'; type MethodDescription = { @@ -25,6 +25,12 @@ class Method extends ExpressionEntity { super(); } + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelf) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } + } + getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity { if (path.length > 0) { return UNKNOWN_EXPRESSION; @@ -73,10 +79,6 @@ class Method extends ExpressionEntity { arg.include(context, false); } } - - mayModifyThisWhenCalledAtPath(path: ObjectPath): boolean { - return path.length === 0 && this.description.mutatesSelf; - } } export const METHOD_RETURNS_BOOLEAN = new Method({ diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index e57576d20a6..4b96d01808a 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -11,6 +11,7 @@ import { import { ExpressionEntity, LiteralValueOrUnknown, + NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './Expression'; @@ -95,6 +96,39 @@ export class ObjectEntity extends ExpressionEntity { this.prototypeExpression?.deoptimizePath(path.length === 1 ? [UnknownKey, UnknownKey] : path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (path.length === 0) { + return; + } + const key = path[0]; + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, { + deoptimizeCache() { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } + }); + if (expressionAtPath) { + return expressionAtPath.deoptimizeThisOnEventAtPath( + event, + path.slice(1), + thisParameter, + recursionTracker + ); + } + if (this.prototypeExpression) { + return this.prototypeExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -217,29 +251,6 @@ export class ObjectEntity extends ExpressionEntity { return true; } - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - if (path.length === 0) { - return false; - } - const key = path[0]; - const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, origin); - if (expressionAtPath) { - return expressionAtPath.mayModifyThisWhenCalledAtPath( - path.slice(1), - recursionTracker, - origin - ); - } - if (this.prototypeExpression) { - return this.prototypeExpression.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - } - return false; - } - private buildPropertyMaps(properties: ObjectProperty[]): void { const { allProperties, diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index bcdd1570961..9863182254e 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -2,7 +2,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './Expression'; export class ObjectMember extends ExpressionEntity { constructor(private readonly object: ExpressionEntity, private readonly key: string) { @@ -13,6 +13,15 @@ export class ObjectMember extends ExpressionEntity { this.object.deoptimizePath([this.key, ...path]); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.object.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -49,12 +58,4 @@ export class ObjectMember extends ExpressionEntity { ): boolean { return this.object.hasEffectsWhenCalledAtPath([this.key, ...path], callOptions, context); } - - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ): boolean { - return this.object.mayModifyThisWhenCalledAtPath([this.key, ...path], recursionTracker, origin); - } } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 268fb83d9fb..af155be8d0a 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -5,7 +5,13 @@ import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UnknownValue, + UNKNOWN_EXPRESSION +} from '../nodes/shared/Expression'; import { ExpressionNode, Node } from '../nodes/shared/Node'; import SpreadElement from '../nodes/SpreadElement'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../utils/PathTracker'; @@ -23,7 +29,7 @@ export default class LocalVariable extends Variable { // Caching and deoptimization: // We track deoptimization when we do not return something unknown - private deoptimizationTracker: PathTracker; + protected deoptimizationTracker: PathTracker; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; constructor( @@ -62,6 +68,7 @@ export default class LocalVariable extends Variable { deoptimizePath(path: ObjectPath) { if (path.length > MAX_PATH_DEPTH || this.isReassigned) return; + // TODO Lukas how about trackEntityAtPathAndGetIfTracked? const trackedEntities = this.deoptimizationTracker.getEntities(path); if (trackedEntities.has(this)) return; trackedEntities.add(this); @@ -82,6 +89,28 @@ export default class LocalVariable extends Variable { } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ): void { + if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { + return thisParameter.deoptimizePath(UNKNOWN_PATH); + } + const trackedEntities = recursionTracker.getEntities(path); + if (!trackedEntities.has(this.init)) { + trackedEntities.add(this.init); + this.init?.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + trackedEntities.delete(this.init); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -187,22 +216,4 @@ export default class LocalVariable extends Variable { markCalledFromTryStatement() { this.calledFromTryStatement = true; } - - mayModifyThisWhenCalledAtPath( - path: ObjectPath, - recursionTracker: PathTracker, - origin: DeoptimizableEntity - ) { - if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { - return true; - } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return true; - } - trackedEntities.add(this.init); - const result = this.init.mayModifyThisWhenCalledAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return result; - } } diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 3294216f6c5..bdede2a4ac0 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,15 +1,41 @@ import { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; +import { + ExpressionEntity, + LiteralValueOrUnknown, + UnknownValue, + UNKNOWN_EXPRESSION +} from '../nodes/shared/Expression'; import { ObjectPath } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; export default class ThisVariable extends LocalVariable { + private deoptimizedPaths: ObjectPath[] = []; + private entitiesToBeDeoptimized: ExpressionEntity[] = []; + constructor(context: AstContext) { super('this', null, null, context); } + addEntityToBeDeoptimized(entity: ExpressionEntity) { + for (const path of this.deoptimizedPaths) { + entity.deoptimizePath(path); + } + this.entitiesToBeDeoptimized.push(entity); + } + + deoptimizePath(path: ObjectPath) { + if (path.length === 0) return; + const trackedEntities = this.deoptimizationTracker.getEntities(path); + if (trackedEntities.has(this)) return; + trackedEntities.add(this); + this.deoptimizedPaths.push(path); + for (const entity of this.entitiesToBeDeoptimized) { + entity.deoptimizePath(path); + } + } + getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } diff --git a/test/form/samples/minimal-this-mutation/_config.js b/test/form/samples/minimal-this-mutation/_config.js new file mode 100644 index 00000000000..d0cecf5de3d --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'mutates "this" only if necessary on function calls' +}; diff --git a/test/form/samples/minimal-this-mutation/_expected.js b/test/form/samples/minimal-this-mutation/_expected.js new file mode 100644 index 00000000000..bf850340e03 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/_expected.js @@ -0,0 +1,48 @@ +console.log('retained'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +console.log('retained'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +console.log('retained'); diff --git a/test/form/samples/minimal-this-mutation/main.js b/test/form/samples/minimal-this-mutation/main.js new file mode 100644 index 00000000000..a70c3d521e5 --- /dev/null +++ b/test/form/samples/minimal-this-mutation/main.js @@ -0,0 +1,56 @@ +const obj1 = { + noMutation() {}, + prop: true +}; +obj1.noMutation(); +if (obj1.prop) console.log('retained'); +else console.log('removed'); + +const obj2 = { + noMutationEffect() { + console.log('effect'); + }, + prop: true +}; +obj2.noMutationEffect(); +if (obj2.prop) console.log('retained'); +else console.log('removed'); + +const obj3 = { + mutateProp() { + this.prop = false; + }, + prop: true +}; +obj3.mutateProp(); +if (obj3.prop) console.log('unimportant'); +else console.log('retained'); + +const obj4 = { + mutateUnknownProp() { + this[globalThis.unknown] = false; + }, + prop: true +}; +obj4.mutateUnknownProp(); +if (obj4.prop) console.log('retained'); +else console.log('retained'); + +const obj5 = { + mutateNestedProp() { + this.nested.prop = false; + }, + nested: { + prop: true + } +}; +obj5.mutateNestedProp(); +if (obj5.nested.prop) console.log('unimportant'); +else console.log('retained'); + +const obj6 = { + prop: true +}; +obj6.doesNotExist(); +if (obj6.prop) console.log('retained'); +else console.log('removed'); From 71a27bacb3b9c01ce220ae7c230d55031d84efa1 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 6 May 2021 20:56:05 +0200 Subject: [PATCH 29/50] Handle deoptimizing "this" in getters --- src/ast/nodes/ArrowFunctionExpression.ts | 1 + src/ast/nodes/CallExpression.ts | 60 +++++---- src/ast/nodes/ConditionalExpression.ts | 20 ++- src/ast/nodes/Identifier.ts | 1 + src/ast/nodes/Literal.ts | 4 +- src/ast/nodes/LogicalExpression.ts | 14 +- src/ast/nodes/MemberExpression.ts | 63 ++++++--- src/ast/nodes/SequenceExpression.ts | 13 +- src/ast/nodes/shared/FunctionNode.ts | 1 + src/ast/nodes/shared/MethodBase.ts | 26 +++- src/ast/nodes/shared/ObjectEntity.ts | 126 +++++++++++++----- src/ast/variables/NamespaceVariable.ts | 3 + .../arrow-function-return-values/_expected.js | 4 - .../conditional-expression-paths/_expected.js | 11 -- .../conditional-expression-paths/main.js | 4 +- .../function-body-return-values/_expected.js | 6 - .../{_expected/es.js => _expected.js} | 0 .../namespace-optimization/_expected/amd.js | 7 - .../namespace-optimization/_expected/cjs.js | 5 - .../namespace-optimization/_expected/iife.js | 8 -- .../_expected/system.js | 12 -- .../namespace-optimization/_expected/umd.js | 10 -- .../samples/namespace-optimization/bar.js | 2 +- .../samples/namespace-optimization/foo.js | 2 +- .../form/samples/recursive-calls/_expected.js | 10 -- .../deoptimized-props-with-getter/_config.js | 11 ++ .../deoptimized-props-with-getter/main.js | 17 +++ .../known-getter/_config.js | 3 + .../known-getter/main.js | 12 ++ .../known-super-prop/_config.js | 3 + .../known-super-prop/main.js | 14 ++ .../unknown-prop-getter/_config.js | 11 ++ .../unknown-prop-getter/main.js | 14 ++ .../unknown-prop-unknown-access/_config.js | 11 ++ .../unknown-prop-unknown-access/main.js | 14 ++ .../unknown-property-access/_config.js | 11 ++ .../unknown-property-access/main.js | 14 ++ .../unknown-super-prop/_config.js | 11 ++ .../unknown-super-prop/main.js | 16 +++ 39 files changed, 411 insertions(+), 164 deletions(-) rename test/form/samples/namespace-optimization/{_expected/es.js => _expected.js} (100%) delete mode 100644 test/form/samples/namespace-optimization/_expected/amd.js delete mode 100644 test/form/samples/namespace-optimization/_expected/cjs.js delete mode 100644 test/form/samples/namespace-optimization/_expected/iife.js delete mode 100644 test/form/samples/namespace-optimization/_expected/system.js delete mode 100644 test/form/samples/namespace-optimization/_expected/umd.js create mode 100644 test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/known-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/known-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/known-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-getter/known-super-prop/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-property-access/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-property-access/main.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-getter/unknown-super-prop/main.js diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index b9d1ba672f4..81d920c7c5f 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -31,6 +31,7 @@ export default class ArrowFunctionExpression extends NodeBase { } } + // TODO Lukas handle nested paths and other event types // Arrow functions do not mutate their context deoptimizeThisOnEventAtPath() {} diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 35752d936fe..aafacc5ea51 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -1,21 +1,11 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; -import { - findFirstOccurrenceOutsideComment, - NodeRenderOptions, - RenderOptions -} from '../../utils/renderHelpers'; +import { findFirstOccurrenceOutsideComment, NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UNKNOWN_PATH -} from '../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; @@ -23,16 +13,11 @@ import { EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, + NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; -import { - Annotation, - ExpressionNode, - IncludeChildren, - INCLUDE_PARAMETERS, - NodeBase -} from './shared/Node'; +import { Annotation, ExpressionNode, IncludeChildren, INCLUDE_PARAMETERS, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -43,8 +28,9 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt type!: NodeType.tCallExpression; private callOptions!: CallOptions; + private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; private deoptimized = false; - private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; + private expressionsToBeDeoptimized: ExpressionEntity[] = []; private returnExpression: ExpressionEntity | null = null; private wasPathDeoptmizedWhileOptimized = false; @@ -80,22 +66,24 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (this.returnExpression !== UNKNOWN_EXPRESSION) { this.returnExpression = null; const returnExpression = this.getReturnExpression(); + const deoptimizableDependentExpressions = this.deoptimizableDependentExpressions; const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; if (returnExpression !== UNKNOWN_EXPRESSION) { // We need to replace here because it is possible new expressions are added // while we are deoptimizing the old ones + this.deoptimizableDependentExpressions = []; this.expressionsToBeDeoptimized = []; if (this.wasPathDeoptmizedWhileOptimized) { returnExpression.deoptimizePath(UNKNOWN_PATH); this.wasPathDeoptmizedWhileOptimized = false; } } - for (const expression of expressionsToBeDeoptimized) { + for (const expression of deoptimizableDependentExpressions) { expression.deoptimizeCache(); } - } - if (this.callee instanceof MemberExpression && !this.callee.variable) { - this.callee.object.deoptimizePath(UNKNOWN_PATH); + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } } } @@ -111,6 +99,26 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + const returnExpression = this.getReturnExpression(recursionTracker); + if (returnExpression === UNKNOWN_EXPRESSION) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } else { + const trackedEntities = recursionTracker.getEntities(path); + if (!trackedEntities.has(returnExpression)) { + this.expressionsToBeDeoptimized.push(thisParameter); + trackedEntities.add(returnExpression); + returnExpression.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + trackedEntities.delete(returnExpression); + } + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, @@ -124,7 +132,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (trackedEntities.has(returnExpression)) { return UnknownValue; } - this.expressionsToBeDeoptimized.push(origin); + this.deoptimizableDependentExpressions.push(origin); trackedEntities.add(returnExpression); const value = returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); trackedEntities.delete(returnExpression); @@ -144,7 +152,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (trackedEntities.has(returnExpression)) { return UNKNOWN_EXPRESSION; } - this.expressionsToBeDeoptimized.push(origin); + this.deoptimizableDependentExpressions.push(origin); trackedEntities.add(returnExpression); const value = returnExpression.getReturnExpressionWhenCalledAtPath( path, diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index fe440cd59a8..72779d5bc20 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -20,7 +20,13 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; +import { + EVENT_CALLED, + ExpressionEntity, + LiteralValueOrUnknown, + NodeEvent, + UnknownValue +} from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -70,6 +76,18 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_CALLED || path.length > 0) { + this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index c2a14105174..b426173770d 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -91,6 +91,7 @@ export default class Identifier extends NodeBase implements PatternNode { thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { + this.bind(); this.variable!.deoptimizeThisOnEventAtPath( event, path, diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index c207a09318b..5183108d398 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -6,7 +6,7 @@ import { getLiteralMembersForValue, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled, - MemberDescription, + MemberDescription } from '../values'; import * as NodeType from './NodeType'; import { LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; @@ -24,6 +24,8 @@ export default class Literal extends Node private members!: { [key: string]: MemberDescription }; + deoptimizeThisOnEventAtPath() {} + getLiteralValueAtPath(path: ObjectPath): LiteralValueOrUnknown { if ( path.length > 0 || diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index f6dfe076c21..d67e2db52c7 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -20,7 +20,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; +import { EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; @@ -70,6 +70,18 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_CALLED || path.length > 0) { + this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 19d7ba4c741..1805ea9d892 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -24,10 +24,12 @@ import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; import { + EVENT_ACCESSED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, - UnknownValue + UnknownValue, + UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -86,9 +88,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE variable: Variable | null = null; private bound = false; + private deoptimized = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; - private wasPathDeoptimizedWhileOptimized = false; bind() { if (this.bound) return; @@ -116,9 +118,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; this.expressionsToBeDeoptimized = []; this.propertyKey = UnknownKey; - if (this.wasPathDeoptimizedWhileOptimized) { - this.object.deoptimizePath(UNKNOWN_PATH); - } + this.object.deoptimizePath(UNKNOWN_PATH); for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } @@ -131,7 +131,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.variable.deoptimizePath(path); } else if (!this.replacement) { const propertyKey = this.getPropertyKey(); - this.wasPathDeoptimizedWhileOptimized = true; this.object.deoptimizePath([propertyKey, ...path]); } } @@ -144,13 +143,8 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE ): void { this.bind(); if (this.variable) { - this.variable.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); - } else { + this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } else if (!this.replacement) { this.object.deoptimizeThisOnEventAtPath( event, [this.propertyKey!, ...path], @@ -169,6 +163,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } + if (this.replacement) { + return UnknownValue; + } this.expressionsToBeDeoptimized.push(origin); return this.object.getLiteralValueAtPath( [this.getPropertyKey(), ...path], @@ -186,6 +183,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); } + if (this.replacement) { + return UNKNOWN_EXPRESSION; + } this.expressionsToBeDeoptimized.push(origin); return this.object.getReturnExpressionWhenCalledAtPath( [this.getPropertyKey(), ...path], @@ -195,12 +195,13 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } hasEffects(context: HasEffectsContext): boolean { - const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) - .propertyReadSideEffects; + if (!this.deoptimized) this.applyDeoptimizations(); + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; return ( this.property.hasEffects(context) || this.object.hasEffects(context) || - // Assignments only access the object before assigning + // Assignments do not access the property before assigning (!(this.parent instanceof AssignmentExpression) && propertyReadSideEffects && (propertyReadSideEffects === 'always' || @@ -213,6 +214,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.hasEffectsWhenAccessedAtPath(path, context); } + if (this.replacement) { + return false; + } return this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!, ...path], context); } @@ -220,6 +224,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.hasEffectsWhenAssignedAtPath(path, context); } + if (this.replacement) { + return true; + } return this.object.hasEffectsWhenAssignedAtPath([this.propertyKey!, ...path], context); } @@ -231,6 +238,9 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable !== null) { return this.variable.hasEffectsWhenCalledAtPath(path, callOptions, context); } + if (this.replacement) { + return true; + } return this.object.hasEffectsWhenCalledAtPath( [this.propertyKey!, ...path], callOptions, @@ -239,6 +249,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -293,6 +304,26 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + private applyDeoptimizations() { + this.deoptimized = true; + const { propertyReadSideEffects } = this.context.options + .treeshake as NormalizedTreeshakingOptions; + // Assignments do not access the property before assigning + if ( + // Namespaces are not bound and should not be deoptimized + this.bound && + propertyReadSideEffects && + !(this.variable || this.replacement || this.parent instanceof AssignmentExpression) + ) { + this.object.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + [this.propertyKey!], + this.object, + SHARED_RECURSION_TRACKER + ); + } + } + private disallowNamespaceReassignment() { if (this.object instanceof Identifier) { const variable = this.scope.findVariable(this.object.name); diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index ed06447e7cb..0e5c4d87695 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -13,7 +13,7 @@ import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { LiteralValueOrUnknown } from './shared/Expression'; +import { EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { @@ -24,6 +24,17 @@ export default class SequenceExpression extends NodeBase { if (path.length > 0) this.expressions[this.expressions.length - 1].deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + if (event === EVENT_CALLED || path.length > 0) { + this.expressions[this.expressions.length - 1].deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + } + } + getLiteralValueAtPath( path: ObjectPath, recursionTracker: PathTracker, diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 280cf569cc6..dfbbcedc3bb 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -40,6 +40,7 @@ export default class FunctionNode extends NodeBase { } } + // TODO Lukas handle other event types as well deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { if (event === EVENT_CALLED) { if (path.length > 0 ) { diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 66611cb7087..bb9f3f0dd5f 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -1,14 +1,12 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER -} from '../../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; import PrivateIdentifier from '../PrivateIdentifier'; import { + EVENT_ACCESSED, + EVENT_ASSIGNED, + EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, @@ -43,6 +41,22 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { + if (event === EVENT_ACCESSED && this.kind === 'get' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } + if (event === EVENT_ASSIGNED && this.kind === 'set' && path.length === 0) { + return this.value.deoptimizeThisOnEventAtPath( + EVENT_CALLED, + EMPTY_PATH, + thisParameter, + recursionTracker + ); + } this.getAccessedValue().deoptimizeThisOnEventAtPath( event, path, diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 4b96d01808a..73e929b289b 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,14 +1,10 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { ObjectPath, ObjectPathKey, PathTracker, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; import { - ObjectPath, - ObjectPathKey, - PathTracker, - UnknownKey, - UNKNOWN_PATH -} from '../../utils/PathTracker'; -import { + EVENT_ACCESSED, + EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, @@ -36,6 +32,7 @@ export class ObjectEntity extends ExpressionEntity { private hasUnknownDeoptimizedProperty = false; private readonly propertiesByKey: PropertyMap = Object.create(null); private readonly settersByKey: PropertyMap = Object.create(null); + private readonly thisParametersToBeDeoptimized = new Set(); private readonly unmatchableGetters: ExpressionEntity[] = []; private readonly unmatchableProperties: ExpressionEntity[] = []; private readonly unmatchableSetters: ExpressionEntity[] = []; @@ -60,6 +57,9 @@ export class ObjectEntity extends ExpressionEntity { expression.deoptimizeCache(); } } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } } deoptimizePath(path: ObjectPath) { @@ -105,27 +105,75 @@ export class ObjectEntity extends ExpressionEntity { if (path.length === 0) { return; } - const key = path[0]; - const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, { - deoptimizeCache() { - thisParameter.deoptimizePath(UNKNOWN_PATH); + const [key, ...subPath] = path; + + if (event === EVENT_CALLED || path.length > 1) { + const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, { + deoptimizeCache() { + thisParameter.deoptimizePath(UNKNOWN_PATH); + } + }); + if (expressionAtPath) { + return expressionAtPath.deoptimizeThisOnEventAtPath( + event, + subPath, + thisParameter, + recursionTracker + ); } - }); - if (expressionAtPath) { - return expressionAtPath.deoptimizeThisOnEventAtPath( - event, - path.slice(1), - thisParameter, - recursionTracker - ); + if (this.prototypeExpression) { + return this.prototypeExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + return; } - if (this.prototypeExpression) { - return this.prototypeExpression.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + if (event === EVENT_ACCESSED) { + if (this.hasUnknownDeoptimizedProperty) { + return thisParameter.deoptimizePath(UNKNOWN_PATH); + } + this.thisParametersToBeDeoptimized.add(thisParameter); + + if (typeof key === 'string') { + for (const property of this.gettersByKey[key] || this.unmatchableGetters) { + property.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + subPath, + thisParameter, + recursionTracker + ); + } + if (this.prototypeExpression && !this.propertiesByKey[key]) { + this.prototypeExpression.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + path, + thisParameter, + recursionTracker + ); + } + } else { + for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { + for (const getter of getters) { + getter.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + subPath, + thisParameter, + recursionTracker + ); + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + path, + thisParameter, + recursionTracker + ); + } + } } } @@ -194,15 +242,23 @@ export class ObjectEntity extends ExpressionEntity { return true; } - // TODO Lukas we could match all getters for unknown paths as well - if (typeof key !== 'string' || this.hasUnknownDeoptimizedProperty) return true; - - const properties = this.gettersByKey[key] || this.unmatchableGetters; - for (const property of properties) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; - } - if (this.prototypeExpression && !this.propertiesByKey[key]) { - return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + if (this.hasUnknownDeoptimizedProperty) return true; + if (typeof key === 'string') { + for (const property of this.gettersByKey[key] || this.unmatchableGetters) { + if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + if (this.prototypeExpression && !this.propertiesByKey[key]) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } + } else { + for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { + for (const getter of getters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + } } return false; } diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 8fb31629a77..b722baa1af3 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -42,6 +42,9 @@ export default class NamespaceVariable extends Variable { } } + // TODO Lukas can this be triggered for nested paths? + deoptimizeThisOnEventAtPath() {} + getMemberVariables(): { [name: string]: Variable } { if (this.memberVariables) { return this.memberVariables; diff --git a/test/form/samples/arrow-function-return-values/_expected.js b/test/form/samples/arrow-function-return-values/_expected.js index 1bedb0a7bbe..a3fd2df3d87 100644 --- a/test/form/samples/arrow-function-return-values/_expected.js +++ b/test/form/samples/arrow-function-return-values/_expected.js @@ -6,9 +6,5 @@ retained1()(); (() => { return () => console.log( 'effect' ); })()(); - -(() => ({ foo: () => {} }))().foo(); (() => ({ foo: () => console.log( 'effect' ) }))().foo(); - -(() => ({ foo: () => ({ bar: () => ({ baz: () => {} }) }) }))().foo().bar().baz(); (() => ({ foo: () => ({ bar: () => console.log( 'effect' ) }) }))().foo().bar(); diff --git a/test/form/samples/conditional-expression-paths/_expected.js b/test/form/samples/conditional-expression-paths/_expected.js index f78090cebcb..ae60637a8b9 100644 --- a/test/form/samples/conditional-expression-paths/_expected.js +++ b/test/form/samples/conditional-expression-paths/_expected.js @@ -1,22 +1,11 @@ var unknownValue = globalThis.unknown(); var foo = { x: () => {}, y: {} }; -var bar = { x: () => {}, y: {} }; var baz = { x: () => console.log('effect') }; -// unknown branch without side-effects -(unknownValue ? foo : bar).y.z; -(unknownValue ? foo : bar).x(); - // unknown branch with side-effect (unknownValue ? foo : baz).y.z; (unknownValue ? foo : baz).x(); -// known branch without side-effects -(foo ).y.z; -(foo).y.z; -(foo ).x(); -(foo).x(); - // known branch with side-effect (baz ).y.z; (baz).y.z; diff --git a/test/form/samples/conditional-expression-paths/main.js b/test/form/samples/conditional-expression-paths/main.js index 67d2162a495..bfafbf296c1 100644 --- a/test/form/samples/conditional-expression-paths/main.js +++ b/test/form/samples/conditional-expression-paths/main.js @@ -19,8 +19,8 @@ var d3 = (false ? baz : foo).x(); var foo2 = { y: {} }; var baz2 = {}; -(true ? foo2 : baz).y.z = 1; -(false ? baz : foo2).y.z = 1; +(true ? foo2 : baz2).y.z = 1; +(false ? baz2 : foo2).y.z = 1; // known branch with side-effect var a4 = (true ? baz : foo).y.z; diff --git a/test/form/samples/function-body-return-values/_expected.js b/test/form/samples/function-body-return-values/_expected.js index cd4f77d4ce6..0064a3f4c4a 100644 --- a/test/form/samples/function-body-return-values/_expected.js +++ b/test/form/samples/function-body-return-values/_expected.js @@ -1,9 +1,3 @@ -function removed3 () { - return { x: () => {} }; -} - -removed3().x(); - function retained1 () { return () => console.log( 'effect' ); } diff --git a/test/form/samples/namespace-optimization/_expected/es.js b/test/form/samples/namespace-optimization/_expected.js similarity index 100% rename from test/form/samples/namespace-optimization/_expected/es.js rename to test/form/samples/namespace-optimization/_expected.js diff --git a/test/form/samples/namespace-optimization/_expected/amd.js b/test/form/samples/namespace-optimization/_expected/amd.js deleted file mode 100644 index 95231f1878b..00000000000 --- a/test/form/samples/namespace-optimization/_expected/amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define(function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}); diff --git a/test/form/samples/namespace-optimization/_expected/cjs.js b/test/form/samples/namespace-optimization/_expected/cjs.js deleted file mode 100644 index 33a83e92c84..00000000000 --- a/test/form/samples/namespace-optimization/_expected/cjs.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -function a () {} - -console.log( a() ); diff --git a/test/form/samples/namespace-optimization/_expected/iife.js b/test/form/samples/namespace-optimization/_expected/iife.js deleted file mode 100644 index 64673c93d43..00000000000 --- a/test/form/samples/namespace-optimization/_expected/iife.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - 'use strict'; - - function a () {} - - console.log( a() ); - -}()); diff --git a/test/form/samples/namespace-optimization/_expected/system.js b/test/form/samples/namespace-optimization/_expected/system.js deleted file mode 100644 index dacc31b2984..00000000000 --- a/test/form/samples/namespace-optimization/_expected/system.js +++ /dev/null @@ -1,12 +0,0 @@ -System.register([], function () { - 'use strict'; - return { - execute: function () { - - function a () {} - - console.log( a() ); - - } - }; -}); diff --git a/test/form/samples/namespace-optimization/_expected/umd.js b/test/form/samples/namespace-optimization/_expected/umd.js deleted file mode 100644 index 2163f63c851..00000000000 --- a/test/form/samples/namespace-optimization/_expected/umd.js +++ /dev/null @@ -1,10 +0,0 @@ -(function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - factory(); -}((function () { 'use strict'; - - function a () {} - - console.log( a() ); - -}))); diff --git a/test/form/samples/namespace-optimization/bar.js b/test/form/samples/namespace-optimization/bar.js index aa96676744d..ad92a1a3d6b 100644 --- a/test/form/samples/namespace-optimization/bar.js +++ b/test/form/samples/namespace-optimization/bar.js @@ -1,3 +1,3 @@ import * as quux from './quux'; -export { quux }; +export { quux }; diff --git a/test/form/samples/namespace-optimization/foo.js b/test/form/samples/namespace-optimization/foo.js index 42a93ae6fe9..d7132d920d3 100644 --- a/test/form/samples/namespace-optimization/foo.js +++ b/test/form/samples/namespace-optimization/foo.js @@ -1,3 +1,3 @@ import * as bar from './bar'; -export { bar }; +export { bar }; diff --git a/test/form/samples/recursive-calls/_expected.js b/test/form/samples/recursive-calls/_expected.js index 00f0e5d1be2..b5e78683edd 100644 --- a/test/form/samples/recursive-calls/_expected.js +++ b/test/form/samples/recursive-calls/_expected.js @@ -1,13 +1,3 @@ -const removed4 = () => globalThis.unknown ? removed4() : { x: () => {} }; -removed4().x(); - -const removed8 = { - get x () { - return globalThis.unknown ? removed8.x : { y: () => {} }; - } -}; -removed8.x.y(); - const retained1 = () => globalThis.unknown ? retained1() : console.log( 'effect' ); retained1(); diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js new file mode 100644 index 00000000000..def94e30b12 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/deoptimized-props-with-getter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + get() { + this.flag = true; + } +}); + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-getter/_config.js b/test/function/samples/modify-this-via-getter/known-getter/_config.js new file mode 100644 index 00000000000..b608017f8e2 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-getter/known-getter/main.js b/test/function/samples/modify-this-via-getter/known-getter/main.js new file mode 100644 index 00000000000..9fa5e548b09 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-getter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/_config.js b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js new file mode 100644 index 00000000000..479fdabd7b6 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for known properties', +}; diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/main.js b/test/function/samples/modify-this-via-getter/known-super-prop/main.js new file mode 100644 index 00000000000..ca4cd5a954c --- /dev/null +++ b/test/function/samples/modify-this-via-getter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js new file mode 100644 index 00000000000..4af96c05a70 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js new file mode 100644 index 00000000000..c8829e86915 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-getter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj.prop; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..884460d181b --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..40846145270 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + get [unknown]() { + this.flag = true; + }, + flag: false +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js new file mode 100644 index 00000000000..98690a2ad8d --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles getters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-property-access/main.js b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js new file mode 100644 index 00000000000..0c790f0347f --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-property-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + flag: false, + get prop() { + this.flag = true; + } +}; + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..362170a081b --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles getters that modify "this" on prototypes for unknown properties', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js new file mode 100644 index 00000000000..9530c344172 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static get prop() { + this.flag = true; + } +} + +class obj extends proto {} + +obj[unknown]; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} From 616a6d3eac7d95e6cc8a644dd98d1e1d11b30841 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 8 May 2021 07:53:24 +0200 Subject: [PATCH 30/50] Handle deoptimizing "this" in setters --- src/ast/nodes/AssignmentExpression.ts | 4 + src/ast/nodes/CallExpression.ts | 6 +- src/ast/nodes/MemberExpression.ts | 41 ++++--- src/ast/nodes/ThisExpression.ts | 25 +++- src/ast/nodes/shared/ObjectEntity.ts | 107 +++++++++++++----- src/ast/variables/ThisVariable.ts | 45 +++++++- .../nested-deoptimization/_expected.js | 3 +- .../getter-in-assignment/_config.js | 3 + .../getter-in-assignment/main.js | 14 +++ .../getters-on-this/_config.js | 3 + .../getters-on-this/main.js | 39 +++++++ .../known-super-prop/_config.js | 1 + .../unknown-super-prop/_config.js | 1 + .../deoptimized-props-with-setter/_config.js | 11 ++ .../deoptimized-props-with-setter/main.js | 17 +++ .../known-setter/_config.js | 3 + .../known-setter/main.js | 12 ++ .../known-super-prop/_config.js | 4 + .../known-super-prop/main.js | 14 +++ .../unknown-prop-setter/_config.js | 11 ++ .../unknown-prop-setter/main.js | 14 +++ .../unknown-prop-unknown-access/_config.js | 11 ++ .../unknown-prop-unknown-access/main.js | 14 +++ .../unknown-super-prop/_config.js | 12 ++ .../unknown-super-prop/main.js | 16 +++ 25 files changed, 375 insertions(+), 56 deletions(-) create mode 100644 test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js create mode 100644 test/function/samples/modify-this-via-getter/getter-in-assignment/main.js create mode 100644 test/function/samples/modify-this-via-getter/getters-on-this/_config.js create mode 100644 test/function/samples/modify-this-via-getter/getters-on-this/main.js create mode 100644 test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/known-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/known-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/known-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-setter/known-super-prop/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js create mode 100644 test/function/samples/modify-this-via-setter/unknown-super-prop/main.js diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index f982147be34..250b507b1e6 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -122,9 +122,13 @@ export default class AssignmentExpression extends NodeBase { } } + // TODO Lukas for operators != "=" we can also trigger getters + // This exception needs to be added to MemberExpression + // TODO Lukas is it time for propertyWriteSideEffects? private applyDeoptimizations() { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index aafacc5ea51..0c74c093949 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -30,7 +30,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt private callOptions!: CallOptions; private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; private deoptimized = false; - private expressionsToBeDeoptimized: ExpressionEntity[] = []; + private expressionsToBeDeoptimized = new Set(); private returnExpression: ExpressionEntity | null = null; private wasPathDeoptmizedWhileOptimized = false; @@ -72,7 +72,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt // We need to replace here because it is possible new expressions are added // while we are deoptimizing the old ones this.deoptimizableDependentExpressions = []; - this.expressionsToBeDeoptimized = []; + this.expressionsToBeDeoptimized = new Set(); if (this.wasPathDeoptmizedWhileOptimized) { returnExpression.deoptimizePath(UNKNOWN_PATH); this.wasPathDeoptmizedWhileOptimized = false; @@ -111,7 +111,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } else { const trackedEntities = recursionTracker.getEntities(path); if (!trackedEntities.has(returnExpression)) { - this.expressionsToBeDeoptimized.push(thisParameter); + this.expressionsToBeDeoptimized.add(thisParameter); trackedEntities.add(returnExpression); returnExpression.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); trackedEntities.delete(returnExpression); diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 1805ea9d892..2c1875d621b 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -12,8 +12,8 @@ import { ObjectPathKey, PathTracker, SHARED_RECURSION_TRACKER, - UnknownKey, - UNKNOWN_PATH + UNKNOWN_PATH, + UnknownKey } from '../utils/PathTracker'; import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; @@ -25,11 +25,12 @@ import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; import { EVENT_ACCESSED, + EVENT_ASSIGNED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, - UnknownValue, - UNKNOWN_EXPRESSION + UNKNOWN_EXPRESSION, + UnknownValue } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -202,7 +203,11 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.property.hasEffects(context) || this.object.hasEffects(context) || // Assignments do not access the property before assigning - (!(this.parent instanceof AssignmentExpression) && + (!( + this.variable || + this.replacement || + (this.parent instanceof AssignmentExpression && this.parent.operator === '=') + ) && propertyReadSideEffects && (propertyReadSideEffects === 'always' || this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context))) @@ -308,19 +313,29 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.deoptimized = true; const { propertyReadSideEffects } = this.context.options .treeshake as NormalizedTreeshakingOptions; - // Assignments do not access the property before assigning if ( // Namespaces are not bound and should not be deoptimized this.bound && propertyReadSideEffects && - !(this.variable || this.replacement || this.parent instanceof AssignmentExpression) + !(this.variable || this.replacement) ) { - this.object.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - [this.propertyKey!], - this.object, - SHARED_RECURSION_TRACKER - ); + // Regular Assignments do not access the property before assigning + if (!(this.parent instanceof AssignmentExpression && this.parent.operator === '=')) { + this.object.deoptimizeThisOnEventAtPath( + EVENT_ACCESSED, + [this.propertyKey!], + this.object, + SHARED_RECURSION_TRACKER + ); + } + if (this.parent instanceof AssignmentExpression) { + this.object.deoptimizeThisOnEventAtPath( + EVENT_ASSIGNED, + [this.propertyKey!], + this.object, + SHARED_RECURSION_TRACKER + ); + } } } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index ba580fd0ce7..c922d9323c9 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -1,22 +1,23 @@ import MagicString from 'magic-string'; import { HasEffectsContext } from '../ExecutionContext'; import ModuleScope from '../scopes/ModuleScope'; -import { ObjectPath } from '../utils/PathTracker'; -import ThisVariable from '../variables/ThisVariable'; +import { ObjectPath, PathTracker } from '../utils/PathTracker'; +import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; +import { ExpressionEntity, NodeEvent } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { type!: NodeType.tThisExpression; - variable!: ThisVariable; + variable!: Variable; private alias!: string | null; private bound = false; bind() { if (this.bound) return; this.bound = true; - this.variable = this.scope.findVariable('this') as ThisVariable; + this.variable = this.scope.findVariable('this'); } deoptimizePath(path: ObjectPath) { @@ -24,6 +25,22 @@ export default class ThisExpression extends NodeBase { this.variable.deoptimizePath(path); } + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + this.bind(); + this.variable.deoptimizeThisOnEventAtPath( + event, + path, + // We rewrite the parameter so that a ThisVariable can detect self-mutations + thisParameter === this ? this.variable : thisParameter, + recursionTracker + ); + } + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 && this.variable.hasEffectsWhenAccessedAtPath(path, context); } diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 73e929b289b..08bbdbb5a51 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,7 +1,13 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; -import { ObjectPath, ObjectPathKey, PathTracker, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; +import { + ObjectPath, + ObjectPathKey, + PathTracker, + UnknownKey, + UNKNOWN_PATH +} from '../../utils/PathTracker'; import { EVENT_ACCESSED, EVENT_CALLED, @@ -30,11 +36,12 @@ export class ObjectEntity extends ExpressionEntity { > = Object.create(null); private readonly gettersByKey: PropertyMap = Object.create(null); private hasUnknownDeoptimizedProperty = false; - private readonly propertiesByKey: PropertyMap = Object.create(null); + private readonly propertiesAndGettersByKey: PropertyMap = Object.create(null); + private readonly propertiesAndSettersByKey: PropertyMap = Object.create(null); private readonly settersByKey: PropertyMap = Object.create(null); private readonly thisParametersToBeDeoptimized = new Set(); private readonly unmatchableGetters: ExpressionEntity[] = []; - private readonly unmatchableProperties: ExpressionEntity[] = []; + private readonly unmatchablePropertiesAndGetters: ExpressionEntity[] = []; private readonly unmatchableSetters: ExpressionEntity[] = []; constructor(properties: ObjectProperty[], private prototypeExpression: ExpressionEntity | null) { @@ -86,7 +93,7 @@ export class ObjectEntity extends ExpressionEntity { const subPath = path.length === 1 ? UNKNOWN_PATH : path.slice(1); for (const property of typeof key === 'string' - ? (this.propertiesByKey[key] || this.unmatchableProperties).concat( + ? (this.propertiesAndGettersByKey[key] || this.unmatchablePropertiesAndGetters).concat( this.settersByKey[key] || this.unmatchableSetters ) : this.allProperties) { @@ -108,6 +115,7 @@ export class ObjectEntity extends ExpressionEntity { const [key, ...subPath] = path; if (event === EVENT_CALLED || path.length > 1) { + // TODO Lukas having the same logic as for the other cases here by checking getters and props const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, { deoptimizeCache() { thisParameter.deoptimizePath(UNKNOWN_PATH); @@ -129,9 +137,8 @@ export class ObjectEntity extends ExpressionEntity { recursionTracker ); } - return; - } - if (event === EVENT_ACCESSED) { + // TODO Lukas check in how far those cases can be merged + } else if (event === EVENT_ACCESSED) { if (this.hasUnknownDeoptimizedProperty) { return thisParameter.deoptimizePath(UNKNOWN_PATH); } @@ -146,7 +153,7 @@ export class ObjectEntity extends ExpressionEntity { recursionTracker ); } - if (this.prototypeExpression && !this.propertiesByKey[key]) { + if (this.prototypeExpression && !this.propertiesAndGettersByKey[key]) { this.prototypeExpression.deoptimizeThisOnEventAtPath( EVENT_ACCESSED, path, @@ -174,6 +181,39 @@ export class ObjectEntity extends ExpressionEntity { ); } } + } else { + if (this.hasUnknownDeoptimizedProperty) { + return thisParameter.deoptimizePath(UNKNOWN_PATH); + } + this.thisParametersToBeDeoptimized.add(thisParameter); + + if (typeof key === 'string') { + for (const property of this.settersByKey[key] || this.unmatchableSetters) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + if (this.prototypeExpression && !this.propertiesAndSettersByKey[key]) { + this.prototypeExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + } else { + for (const setters of Object.values(this.settersByKey).concat([this.unmatchableSetters])) { + for (const setter of setters) { + setter.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } + if (this.prototypeExpression) { + return this.prototypeExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + } + } } } @@ -247,7 +287,7 @@ export class ObjectEntity extends ExpressionEntity { for (const property of this.gettersByKey[key] || this.unmatchableGetters) { if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; } - if (this.prototypeExpression && !this.propertiesByKey[key]) { + if (this.prototypeExpression && !this.propertiesAndGettersByKey[key]) { return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); } } else { @@ -310,36 +350,43 @@ export class ObjectEntity extends ExpressionEntity { private buildPropertyMaps(properties: ObjectProperty[]): void { const { allProperties, - propertiesByKey, + propertiesAndGettersByKey, + propertiesAndSettersByKey, settersByKey, gettersByKey, - unmatchableProperties, + unmatchablePropertiesAndGetters, unmatchableGetters, unmatchableSetters } = this; + const unmatchablePropertiesAndSetters: ExpressionEntity[] = []; for (let index = properties.length - 1; index >= 0; index--) { const { key, kind, property } = properties[index]; allProperties.push(property); - if (kind === 'set') { - if (typeof key !== 'string') { - unmatchableSetters.push(property); - } else if (!settersByKey[key]) { - settersByKey[key] = [property, ...unmatchableSetters]; - } - } else { if (typeof key !== 'string') { - unmatchableProperties.push(property); - if (kind === 'get') { - unmatchableGetters.push(property); - } - } else if (!propertiesByKey[key]) { - propertiesByKey[key] = [property, ...unmatchableProperties]; - gettersByKey[key] = [...unmatchableGetters]; - if (kind === 'get') { - gettersByKey[key].push(property); + if (kind === 'set') unmatchableSetters.push(property) + if (kind === 'get') unmatchableGetters.push(property) + if (kind !== 'get') unmatchablePropertiesAndSetters.push(property) + if (kind !== 'set') unmatchablePropertiesAndGetters.push(property) + } else { + if (kind === 'set') { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters] + settersByKey[key] = [property, ...unmatchableSetters] + } + } else if (kind === 'get') { + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters] + gettersByKey[key] = [property, ...unmatchableGetters] + } + } else { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters] + } + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters] + } } } - } } } @@ -351,11 +398,11 @@ export class ObjectEntity extends ExpressionEntity { ) { return UNKNOWN_EXPRESSION; } - const properties = this.propertiesByKey[key]; + const properties = this.propertiesAndGettersByKey[key]; if (properties?.length === 1) { return properties[0]; } - if (properties || this.unmatchableProperties.length > 0) { + if (properties || this.unmatchablePropertiesAndGetters.length > 0) { return UNKNOWN_EXPRESSION; } return null; diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index bdede2a4ac0..e5fa873b4a8 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -4,15 +4,23 @@ import { HasEffectsContext } from '../ExecutionContext'; import { ExpressionEntity, LiteralValueOrUnknown, - UnknownValue, - UNKNOWN_EXPRESSION + NodeEvent, + UNKNOWN_EXPRESSION, + UnknownValue } from '../nodes/shared/Expression'; -import { ObjectPath } from '../utils/PathTracker'; +import { ObjectPath, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; +interface ThisDeoptimizationEvent { + event: NodeEvent; + path: ObjectPath; + thisParameter: ExpressionEntity; +} + export default class ThisVariable extends LocalVariable { private deoptimizedPaths: ObjectPath[] = []; - private entitiesToBeDeoptimized: ExpressionEntity[] = []; + private entitiesToBeDeoptimized = new Set(); + private thisDeoptimizations: ThisDeoptimizationEvent[] = []; constructor(context: AstContext) { super('this', null, null, context); @@ -22,7 +30,10 @@ export default class ThisVariable extends LocalVariable { for (const path of this.deoptimizedPaths) { entity.deoptimizePath(path); } - this.entitiesToBeDeoptimized.push(entity); + for (const thisDeoptimization of this.thisDeoptimizations) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.entitiesToBeDeoptimized.add(entity); } deoptimizePath(path: ObjectPath) { @@ -36,6 +47,18 @@ export default class ThisVariable extends LocalVariable { } } + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { + const thisDeoptimization: ThisDeoptimizationEvent = { + event, + path, + thisParameter + }; + for (const entity of this.entitiesToBeDeoptimized) { + this.applyThisDeoptimizationEvent(entity, thisDeoptimization); + } + this.thisDeoptimizations.push(thisDeoptimization); + } + getLiteralValueAtPath(): LiteralValueOrUnknown { return UnknownValue; } @@ -65,6 +88,18 @@ export default class ThisVariable extends LocalVariable { ); } + private applyThisDeoptimizationEvent( + entity: ExpressionEntity, + { event, path, thisParameter }: ThisDeoptimizationEvent + ) { + entity.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter === this ? entity : thisParameter, + SHARED_RECURSION_TRACKER + ); + } + private getInit(context: HasEffectsContext): ExpressionEntity { return context.replacedVariableInits.get(this) || UNKNOWN_EXPRESSION; } diff --git a/test/form/samples/nested-deoptimization/_expected.js b/test/form/samples/nested-deoptimization/_expected.js index 04bb1a1620c..511e834d27a 100644 --- a/test/form/samples/nested-deoptimization/_expected.js +++ b/test/form/samples/nested-deoptimization/_expected.js @@ -7,5 +7,6 @@ obj[globalThis.unknown].prop = false; if (obj.foo.prop) console.log('retained'); else console.log('also retained'); -console.log('retained'); +if (obj.bar.otherProp) console.log('retained'); +else console.log('removed'); console.log('retained'); diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js new file mode 100644 index 00000000000..4f3496ac6bc --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known getters that are triggered via an assignment' +}; diff --git a/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js new file mode 100644 index 00000000000..4244e759d89 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getter-in-assignment/main.js @@ -0,0 +1,14 @@ +const obj = { + flag: false, + get prop() { + this.flag = true; + return 1; + }, + set prop(v) {} +}; + +obj.prop += 1; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/_config.js b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js new file mode 100644 index 00000000000..6bd8c9213a8 --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles calling getters on "this"', +}; diff --git a/test/function/samples/modify-this-via-getter/getters-on-this/main.js b/test/function/samples/modify-this-via-getter/getters-on-this/main.js new file mode 100644 index 00000000000..24f3b72fdbd --- /dev/null +++ b/test/function/samples/modify-this-via-getter/getters-on-this/main.js @@ -0,0 +1,39 @@ +const obj = { + flag1: false, + flag2: false, + flag3: false, + flag4: false, + prop1() { + this.flag1 = true; + this.prop2(); + }, + prop2() { + this.flag2 = true; + this.prop3; + }, + get prop3() { + this.flag3 = true; + this.prop4 = true; + }, + set prop4(value) { + this.flag4 = value; + } +}; + +obj.prop1(); + +if (!obj.flag1) { + throw new Error('mutation 1 not detected'); +} + +if (!obj.flag2) { + throw new Error('mutation 2 not detected'); +} + +if (!obj.flag3) { + throw new Error('mutation 3 not detected'); +} + +if (!obj.flag4) { + throw new Error('mutation 4 not detected'); +} diff --git a/test/function/samples/modify-this-via-getter/known-super-prop/_config.js b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js index 479fdabd7b6..52280f050d4 100644 --- a/test/function/samples/modify-this-via-getter/known-super-prop/_config.js +++ b/test/function/samples/modify-this-via-getter/known-super-prop/_config.js @@ -1,3 +1,4 @@ module.exports = { description: 'handles getters that modify "this" on prototypes for known properties', + minNodeVersion: 12 }; diff --git a/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js index 362170a081b..7e4188cbd13 100644 --- a/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js +++ b/test/function/samples/modify-this-via-getter/unknown-super-prop/_config.js @@ -1,5 +1,6 @@ module.exports = { description: 'handles getters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, context: { require(id) { return { unknown: 'prop' }; diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js new file mode 100644 index 00000000000..4b7c9b3ebb3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles fully deoptimized objects', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js new file mode 100644 index 00000000000..73c7d3a1af3 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/deoptimized-props-with-setter/main.js @@ -0,0 +1,17 @@ +import { unknown } from 'external'; + +const obj = { + flag: false +}; + +Object.defineProperty(obj, unknown, { + set(value) { + this.flag = value; + } +}); + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-setter/_config.js b/test/function/samples/modify-this-via-setter/known-setter/_config.js new file mode 100644 index 00000000000..449c895ba6d --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles known setters that modify "this"' +}; diff --git a/test/function/samples/modify-this-via-setter/known-setter/main.js b/test/function/samples/modify-this-via-setter/known-setter/main.js new file mode 100644 index 00000000000..74930dba6fb --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-setter/main.js @@ -0,0 +1,12 @@ +const obj = { + flag: false, + set prop(value) { + this.flag = value; + } +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/_config.js b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js new file mode 100644 index 00000000000..1a2b7ea0df4 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for known properties', + minNodeVersion: 12 +}; diff --git a/test/function/samples/modify-this-via-setter/known-super-prop/main.js b/test/function/samples/modify-this-via-setter/known-super-prop/main.js new file mode 100644 index 00000000000..d6f72d34575 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/known-super-prop/main.js @@ -0,0 +1,14 @@ +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js new file mode 100644 index 00000000000..d6305f6c204 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this"', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js new file mode 100644 index 00000000000..4285ed33781 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-setter/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj.prop = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js new file mode 100644 index 00000000000..5b67fd6ea0e --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'handles unknown setters that modify "this" for unknown property access', + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js new file mode 100644 index 00000000000..536e3c7fa79 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-prop-unknown-access/main.js @@ -0,0 +1,14 @@ +import { unknown } from 'external'; + +const obj = { + set [unknown](value) { + this.flag = value; + }, + flag: false +}; + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js new file mode 100644 index 00000000000..c68617f4906 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'handles setters that modify "this" on prototypes for unknown properties', + minNodeVersion: 12, + context: { + require(id) { + return { unknown: 'prop' }; + } + }, + options: { + external: ['external'] + } +}; diff --git a/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js new file mode 100644 index 00000000000..f1e106b4b70 --- /dev/null +++ b/test/function/samples/modify-this-via-setter/unknown-super-prop/main.js @@ -0,0 +1,16 @@ +import { unknown } from 'external'; + +class proto { + static flag = false; + static set prop(value) { + this.flag = value; + } +} + +class obj extends proto {} + +obj[unknown] = true; + +if (!obj.flag) { + throw new Error('mutation not detected'); +} From e33e06efa47bfb09c2046e8ab9d517a0cfd81023 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 8 May 2021 21:43:34 +0200 Subject: [PATCH 31/50] Unify this deoptimization --- src/ast/nodes/MemberExpression.ts | 8 +- src/ast/nodes/ObjectExpression.ts | 8 +- src/ast/nodes/shared/ObjectEntity.ts | 179 +++++++----------- src/ast/variables/ThisVariable.ts | 4 +- .../nested-deoptimization/_expected.js | 3 +- 5 files changed, 72 insertions(+), 130 deletions(-) diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 2c1875d621b..070567e60ce 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -12,8 +12,8 @@ import { ObjectPathKey, PathTracker, SHARED_RECURSION_TRACKER, - UNKNOWN_PATH, - UnknownKey + UnknownKey, + UNKNOWN_PATH } from '../utils/PathTracker'; import ExternalVariable from '../variables/ExternalVariable'; import NamespaceVariable from '../variables/NamespaceVariable'; @@ -29,8 +29,8 @@ import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent, - UNKNOWN_EXPRESSION, - UnknownValue + UnknownValue, + UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 3f149d5221b..300678e4081 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,13 +4,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UnknownKey -} from '../utils/PathTracker'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 08bbdbb5a51..8db18c5c31a 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -99,7 +99,6 @@ export class ObjectEntity extends ExpressionEntity { : this.allProperties) { property.deoptimizePath(subPath); } - // TODO Lukas only if we have no hit here, we need to continue with the prototype this.prototypeExpression?.deoptimizePath(path.length === 1 ? [UnknownKey, UnknownKey] : path); } @@ -114,107 +113,57 @@ export class ObjectEntity extends ExpressionEntity { } const [key, ...subPath] = path; - if (event === EVENT_CALLED || path.length > 1) { - // TODO Lukas having the same logic as for the other cases here by checking getters and props - const expressionAtPath = this.getMemberExpressionAndTrackDeopt(key, { - deoptimizeCache() { - thisParameter.deoptimizePath(UNKNOWN_PATH); - } - }); - if (expressionAtPath) { - return expressionAtPath.deoptimizeThisOnEventAtPath( - event, - subPath, - thisParameter, - recursionTracker - ); - } - if (this.prototypeExpression) { - return this.prototypeExpression.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); - } - // TODO Lukas check in how far those cases can be merged - } else if (event === EVENT_ACCESSED) { - if (this.hasUnknownDeoptimizedProperty) { - return thisParameter.deoptimizePath(UNKNOWN_PATH); - } - this.thisParametersToBeDeoptimized.add(thisParameter); + if ( + this.hasUnknownDeoptimizedProperty || + // single paths that are deoptimized will not become getters or setters + ((event === EVENT_CALLED || path.length > 1) && + typeof key === 'string' && + this.deoptimizedPaths[key]) + ) { + thisParameter.deoptimizePath(UNKNOWN_PATH); + return; + } - if (typeof key === 'string') { - for (const property of this.gettersByKey[key] || this.unmatchableGetters) { - property.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - subPath, - thisParameter, - recursionTracker - ); - } - if (this.prototypeExpression && !this.propertiesAndGettersByKey[key]) { - this.prototypeExpression.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - path, - thisParameter, - recursionTracker - ); - } - } else { - for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { - for (const getter of getters) { - getter.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - subPath, - thisParameter, - recursionTracker - ); + this.thisParametersToBeDeoptimized.add(thisParameter); + const [propertiesForExactMatchByKey, relevantPropertiesByKey, relevantUnmatchableProperties] = + event === EVENT_CALLED || path.length > 1 + ? [ + this.propertiesAndGettersByKey, + this.propertiesAndGettersByKey, + this.unmatchablePropertiesAndGetters + ] + : event === EVENT_ACCESSED + ? [this.propertiesAndGettersByKey, this.gettersByKey, this.unmatchableGetters] + : [this.propertiesAndSettersByKey, this.settersByKey, this.unmatchableSetters]; + + if (typeof key === 'string') { + if (propertiesForExactMatchByKey[key]) { + const properties = relevantPropertiesByKey[key]; + if (properties) { + for (const property of properties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } } - if (this.prototypeExpression) { - return this.prototypeExpression.deoptimizeThisOnEventAtPath( - EVENT_ACCESSED, - path, - thisParameter, - recursionTracker - ); - } + return; } - } else { - if (this.hasUnknownDeoptimizedProperty) { - return thisParameter.deoptimizePath(UNKNOWN_PATH); + for (const property of relevantUnmatchableProperties) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } - this.thisParametersToBeDeoptimized.add(thisParameter); - - if (typeof key === 'string') { - for (const property of this.settersByKey[key] || this.unmatchableSetters) { + } else { + for (const properties of Object.values(relevantPropertiesByKey).concat([ + relevantUnmatchableProperties + ])) { + for (const property of properties) { property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } - if (this.prototypeExpression && !this.propertiesAndSettersByKey[key]) { - this.prototypeExpression.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); - } - } else { - for (const setters of Object.values(this.settersByKey).concat([this.unmatchableSetters])) { - for (const setter of setters) { - setter.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); - } - } - if (this.prototypeExpression) { - return this.prototypeExpression.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); - } } } + this.prototypeExpression?.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); } getLiteralValueAtPath( @@ -362,31 +311,31 @@ export class ObjectEntity extends ExpressionEntity { for (let index = properties.length - 1; index >= 0; index--) { const { key, kind, property } = properties[index]; allProperties.push(property); - if (typeof key !== 'string') { - if (kind === 'set') unmatchableSetters.push(property) - if (kind === 'get') unmatchableGetters.push(property) - if (kind !== 'get') unmatchablePropertiesAndSetters.push(property) - if (kind !== 'set') unmatchablePropertiesAndGetters.push(property) + if (typeof key !== 'string') { + if (kind === 'set') unmatchableSetters.push(property); + if (kind === 'get') unmatchableGetters.push(property); + if (kind !== 'get') unmatchablePropertiesAndSetters.push(property); + if (kind !== 'set') unmatchablePropertiesAndGetters.push(property); + } else { + if (kind === 'set') { + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + settersByKey[key] = [property, ...unmatchableSetters]; + } + } else if (kind === 'get') { + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + gettersByKey[key] = [property, ...unmatchableGetters]; + } } else { - if (kind === 'set') { - if (!propertiesAndSettersByKey[key]) { - propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters] - settersByKey[key] = [property, ...unmatchableSetters] - } - } else if (kind === 'get') { - if (!propertiesAndGettersByKey[key]) { - propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters] - gettersByKey[key] = [property, ...unmatchableGetters] - } - } else { - if (!propertiesAndSettersByKey[key]) { - propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters] - } - if (!propertiesAndGettersByKey[key]) { - propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters] - } + if (!propertiesAndSettersByKey[key]) { + propertiesAndSettersByKey[key] = [property, ...unmatchablePropertiesAndSetters]; + } + if (!propertiesAndGettersByKey[key]) { + propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; } } + } } } diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index e5fa873b4a8..e42fc057554 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -5,8 +5,8 @@ import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent, - UNKNOWN_EXPRESSION, - UnknownValue + UnknownValue, + UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; diff --git a/test/form/samples/nested-deoptimization/_expected.js b/test/form/samples/nested-deoptimization/_expected.js index 511e834d27a..04bb1a1620c 100644 --- a/test/form/samples/nested-deoptimization/_expected.js +++ b/test/form/samples/nested-deoptimization/_expected.js @@ -7,6 +7,5 @@ obj[globalThis.unknown].prop = false; if (obj.foo.prop) console.log('retained'); else console.log('also retained'); -if (obj.bar.otherProp) console.log('retained'); -else console.log('removed'); +console.log('retained'); console.log('retained'); From afad828da8025f554a7e71102211fbf43393949b Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 9 May 2021 07:22:42 +0200 Subject: [PATCH 32/50] Simplify recursion tracking --- src/ast/nodes/CallExpression.ts | 118 +++++++++++++++++------------ src/ast/utils/PathTracker.ts | 41 ++++++++-- src/ast/variables/LocalVariable.ts | 87 ++++++++++----------- src/ast/variables/ThisVariable.ts | 10 ++- 4 files changed, 152 insertions(+), 104 deletions(-) diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 0c74c093949..a693e4f5055 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -1,11 +1,21 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { BLANK } from '../../utils/blank'; -import { findFirstOccurrenceOutsideComment, NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; +import { + findFirstOccurrenceOutsideComment, + NodeRenderOptions, + RenderOptions +} from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UNKNOWN_PATH +} from '../utils/PathTracker'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; @@ -17,7 +27,13 @@ import { UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; -import { Annotation, ExpressionNode, IncludeChildren, INCLUDE_PARAMETERS, NodeBase } from './shared/Node'; +import { + Annotation, + ExpressionNode, + IncludeChildren, + INCLUDE_PARAMETERS, + NodeBase +} from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -88,10 +104,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } deoptimizePath(path: ObjectPath) { - if (path.length === 0) return; - const trackedEntities = this.context.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); + if ( + path.length === 0 || + this.context.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } const returnExpression = this.getReturnExpression(); if (returnExpression !== UNKNOWN_EXPRESSION) { this.wasPathDeoptmizedWhileOptimized = true; @@ -109,13 +127,20 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (returnExpression === UNKNOWN_EXPRESSION) { thisParameter.deoptimizePath(UNKNOWN_PATH); } else { - const trackedEntities = recursionTracker.getEntities(path); - if (!trackedEntities.has(returnExpression)) { - this.expressionsToBeDeoptimized.add(thisParameter); - trackedEntities.add(returnExpression); - returnExpression.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - trackedEntities.delete(returnExpression); - } + recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.expressionsToBeDeoptimized.add(thisParameter); + returnExpression.deoptimizeThisOnEventAtPath( + event, + path, + thisParameter, + recursionTracker + ); + }, + undefined + ); } } @@ -128,39 +153,35 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (returnExpression === UNKNOWN_EXPRESSION) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UnknownValue; - } - this.deoptimizableDependentExpressions.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(returnExpression); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( path: ObjectPath, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { + ): ExpressionEntity { const returnExpression = this.getReturnExpression(recursionTracker); if (this.returnExpression === UNKNOWN_EXPRESSION) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(returnExpression)) { - return UNKNOWN_EXPRESSION; - } - this.deoptimizableDependentExpressions.push(origin); - trackedEntities.add(returnExpression); - const value = returnExpression.getReturnExpressionWhenCalledAtPath( + return recursionTracker.withTrackedEntityAtPath( path, - recursionTracker, - origin + returnExpression, + () => { + this.deoptimizableDependentExpressions.push(origin); + return returnExpression.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + }, + UNKNOWN_EXPRESSION ); - trackedEntities.delete(returnExpression); - return value; } hasEffects(context: HasEffectsContext): boolean { @@ -180,18 +201,17 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if (path.length === 0 || context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) { + return false; + } return this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if (context.assigned.trackEntityAtPathAndGetIfTracked(path, this)) { + return false; + } return this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context); } @@ -200,12 +220,14 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callOptions: CallOptions, context: HasEffectsContext ): boolean { - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if ( + (callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) + ) { + return false; + } return this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context); } diff --git a/src/ast/utils/PathTracker.ts b/src/ast/utils/PathTracker.ts index 06a726d5d85..0215fcfc96c 100644 --- a/src/ast/utils/PathTracker.ts +++ b/src/ast/utils/PathTracker.ts @@ -16,10 +16,32 @@ interface EntityPaths { } export class PathTracker { - entityPaths: EntityPaths = Object.create(null, { [EntitiesKey]: { value: new Set() } }); + private entityPaths: EntityPaths = Object.create(null, { + [EntitiesKey]: { value: new Set() } + }); + + trackEntityAtPathAndGetIfTracked(path: ObjectPath, entity: Entity): boolean { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; + } + + withTrackedEntityAtPath( + path: ObjectPath, + entity: Entity, + onUntracked: () => T, + returnIfTracked: T + ): T { + const trackedEntities = this.getEntities(path); + if (trackedEntities.has(entity)) return returnIfTracked; + trackedEntities.add(entity); + const result = onUntracked(); + trackedEntities.delete(entity); + return result; + } - // TODO Lukas can we incorporate the usual usage patterns here? - getEntities(path: ObjectPath): Set { + private getEntities(path: ObjectPath): Set { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = @@ -39,17 +61,24 @@ interface DiscriminatedEntityPaths { } export class DiscriminatedPathTracker { - entityPaths: DiscriminatedEntityPaths = Object.create(null, { + private entityPaths: DiscriminatedEntityPaths = Object.create(null, { [EntitiesKey]: { value: new Map>() } }); - getEntities(path: ObjectPath, discriminator: object): Set { + trackEntityAtPathAndGetIfTracked( + path: ObjectPath, + discriminator: object, + entity: Entity + ): boolean { let currentPaths = this.entityPaths; for (const pathSegment of path) { currentPaths = currentPaths[pathSegment] = currentPaths[pathSegment] || Object.create(null, { [EntitiesKey]: { value: new Map>() } }); } - return getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + const trackedEntities = getOrCreate(currentPaths[EntitiesKey], discriminator, () => new Set()); + if (trackedEntities.has(entity)) return true; + trackedEntities.add(entity); + return false; } } diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index af155be8d0a..02912d713e7 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -67,11 +67,13 @@ export default class LocalVariable extends Variable { } deoptimizePath(path: ObjectPath) { - if (path.length > MAX_PATH_DEPTH || this.isReassigned) return; - // TODO Lukas how about trackEntityAtPathAndGetIfTracked? - const trackedEntities = this.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); + if ( + path.length > MAX_PATH_DEPTH || + this.isReassigned || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } if (path.length === 0) { if (!this.isReassigned) { this.isReassigned = true; @@ -98,17 +100,12 @@ export default class LocalVariable extends Variable { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return thisParameter.deoptimizePath(UNKNOWN_PATH); } - const trackedEntities = recursionTracker.getEntities(path); - if (!trackedEntities.has(this.init)) { - trackedEntities.add(this.init); - this.init?.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); - trackedEntities.delete(this.init); - } + recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => this.init!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker), + undefined + ); } getLiteralValueAtPath( @@ -119,15 +116,15 @@ export default class LocalVariable extends Variable { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UnknownValue; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UnknownValue; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getLiteralValueAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getLiteralValueAtPath(path, recursionTracker, origin); + }, + UnknownValue + ); } getReturnExpressionWhenCalledAtPath( @@ -138,23 +135,21 @@ export default class LocalVariable extends Variable { if (this.isReassigned || !this.init || path.length > MAX_PATH_DEPTH) { return UNKNOWN_EXPRESSION; } - const trackedEntities = recursionTracker.getEntities(path); - if (trackedEntities.has(this.init)) { - return UNKNOWN_EXPRESSION; - } - this.expressionsToBeDeoptimized.push(origin); - trackedEntities.add(this.init); - const value = this.init.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); - trackedEntities.delete(this.init); - return value; + return recursionTracker.withTrackedEntityAtPath( + path, + this.init, + () => { + this.expressionsToBeDeoptimized.push(origin); + return this.init!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + }, + UNKNOWN_EXPRESSION + ); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { if (path.length === 0) return false; if (this.isReassigned || path.length > MAX_PATH_DEPTH) return true; - const trackedExpressions = context.accessed.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if (context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) return false; return (this.init && this.init.hasEffectsWhenAccessedAtPath(path, context))!; } @@ -162,9 +157,7 @@ export default class LocalVariable extends Variable { if (this.included || path.length > MAX_PATH_DEPTH) return true; if (path.length === 0) return false; if (this.isReassigned) return true; - const trackedExpressions = context.assigned.getEntities(path); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if (context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) return false; return (this.init && this.init.hasEffectsWhenAssignedAtPath(path, context))!; } @@ -174,12 +167,14 @@ export default class LocalVariable extends Variable { context: HasEffectsContext ) { if (path.length > MAX_PATH_DEPTH || this.isReassigned) return true; - const trackedExpressions = (callOptions.withNew - ? context.instantiated - : context.called - ).getEntities(path, callOptions); - if (trackedExpressions.has(this)) return false; - trackedExpressions.add(this); + if ( + (callOptions.withNew + ? context.instantiated + : context.called + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) + ) { + return false; + } return (this.init && this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; } diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index e42fc057554..323e3ea1293 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -37,10 +37,12 @@ export default class ThisVariable extends LocalVariable { } deoptimizePath(path: ObjectPath) { - if (path.length === 0) return; - const trackedEntities = this.deoptimizationTracker.getEntities(path); - if (trackedEntities.has(this)) return; - trackedEntities.add(this); + if ( + path.length === 0 || + this.deoptimizationTracker.trackEntityAtPathAndGetIfTracked(path, this) + ) { + return; + } this.deoptimizedPaths.push(path); for (const entity of this.entitiesToBeDeoptimized) { entity.deoptimizePath(path); From d290c6ca852a6e7eea918f2da4624d508ce60b90 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 May 2021 06:35:03 +0200 Subject: [PATCH 33/50] Get rid of deoptimizations during bind phase --- src/ast/nodes/ArrayExpression.ts | 46 ++++++++++++++++++-------- src/ast/nodes/AssignmentExpression.ts | 2 -- src/ast/nodes/AssignmentPattern.ts | 30 ++++++++++++----- src/ast/nodes/ConditionalExpression.ts | 41 ++++++++++++----------- src/ast/nodes/ForInStatement.ts | 15 +++++---- src/ast/nodes/ForOfStatement.ts | 15 +++++---- src/ast/nodes/Identifier.ts | 23 +++++++++---- src/ast/nodes/LogicalExpression.ts | 32 +++++++++--------- src/ast/nodes/MemberExpression.ts | 17 +++++----- src/ast/nodes/NewExpression.ts | 32 ++++++++++++------ src/ast/nodes/Property.ts | 29 ++++++++++------ src/ast/nodes/RestElement.ts | 31 +++++++++++------ src/ast/nodes/SpreadElement.ts | 19 +++++++++-- src/ast/nodes/ThisExpression.ts | 1 + src/ast/nodes/UnaryExpression.ts | 26 ++++++++++----- src/ast/nodes/UpdateExpression.ts | 31 +++++++++++------ src/ast/nodes/YieldExpression.ts | 24 +++++++++----- 17 files changed, 259 insertions(+), 155 deletions(-) diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index d8f95b49fe4..c78ab4f1131 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,32 +1,33 @@ import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; -import { - arrayMembers, - getMemberReturnExpressionWhenCalled, - hasMemberEffectWhenCalled, -} from '../values'; +import { arrayMembers, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled } from '../values'; import * as NodeType from './NodeType'; import { UNKNOWN_EXPRESSION } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; +// TODO Lukas turn into specific object entities export default class ArrayExpression extends NodeBase { elements!: (ExpressionNode | SpreadElement | null)[]; type!: NodeType.tArrayExpression; - - bind() { - super.bind(); - for (const element of this.elements) { - if (element !== null) element.deoptimizePath(UNKNOWN_PATH); - } - } + private deoptimized = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length !== 1) return UNKNOWN_EXPRESSION; return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); } + hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); + for (const element of this.elements) { + if (element && element.hasEffects(context)) { + return true; + } + } + return false; + } + hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } @@ -41,4 +42,21 @@ export default class ArrayExpression extends NodeBase { } return true; } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + for (const element of this.elements) { + if (element) { + element.include(context, includeChildrenRecursively); + } + } + } + + private applyDeoptimizations():void { + this.deoptimized = true; + for (const element of this.elements) { + if (element !== null) element.deoptimizePath(UNKNOWN_PATH); + } + } } diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index 250b507b1e6..4511e3e7c83 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -122,8 +122,6 @@ export default class AssignmentExpression extends NodeBase { } } - // TODO Lukas for operators != "=" we can also trigger getters - // This exception needs to be added to MemberExpression // TODO Lukas is it time for propertyWriteSideEffects? private applyDeoptimizations() { this.deoptimized = true; diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index fc2c92d6b9e..8ebd13ae251 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -1,18 +1,19 @@ import MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class AssignmentPattern extends NodeBase implements PatternNode { left!: PatternNode; right!: ExpressionNode; type!: NodeType.tAssignmentPattern; + private deoptimized = false; addExportedVariables( variables: Variable[], @@ -21,13 +22,6 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.addExportedVariables(variables, exportNamesByVariable); } - // TODO Lukas make all current bind deoptimizations lazy - bind() { - super.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.deoptimizePath(UNKNOWN_PATH); - } - declare(kind: string, init: ExpressionEntity) { return this.left.declare(kind, init); } @@ -36,10 +30,22 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { path.length === 0 && this.left.deoptimizePath(path); } + hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); + return this.left.hasEffects(context) || this.right.hasEffects(context); + } + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.left.include(context, includeChildrenRecursively); + this.right.include(context, includeChildrenRecursively); + } + render( code: MagicString, options: RenderOptions, @@ -48,4 +54,10 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.left.render(code, options, { isShorthandProperty }); this.right.render(code, options); } + + private applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + this.right.deoptimizePath(UNKNOWN_PATH); + } } diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 72779d5bc20..4dd5524b26e 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -42,12 +42,6 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz private usedBranch: ExpressionNode | null = null; private wasPathDeoptimizedWhileOptimized = false; - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } - deoptimizeCache() { if (this.usedBranch !== null) { const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent; @@ -116,32 +110,35 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz hasEffects(context: HasEffectsContext): boolean { if (this.test.hasEffects(context)) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return this.consequent.hasEffects(context) || this.alternate.hasEffects(context); } - return this.usedBranch.hasEffects(context); + return usedBranch.hasEffects(context); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAccessedAtPath(path, context) || this.alternate.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenAssignedAtPath(path, context) || this.alternate.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -149,36 +146,39 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { return ( this.consequent.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.alternate.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; + const usedBranch = this.getUsedBranch(); if ( includeChildrenRecursively || this.test.shouldBeIncluded(context) || - this.usedBranch === null + usedBranch === null ) { this.test.include(context, includeChildrenRecursively); this.consequent.include(context, includeChildrenRecursively); this.alternate.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } includeCallArguments(context: InclusionContext, args: (ExpressionNode | SpreadElement)[]): void { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { this.consequent.includeCallArguments(context, args); this.alternate.includeCallArguments(context, args); } else { - this.usedBranch.includeCallArguments(context, args); + usedBranch.includeCallArguments(context, args); } } @@ -187,6 +187,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz options: RenderOptions, { renderedParentType, isCalleeOfRenderedParent, preventASI }: NodeRenderOptions = BLANK ) { + const usedBranch = this.getUsedBranch(); if (!this.test.included) { const colonPos = findFirstOccurrenceOutsideComment(code.original, ':', this.consequent.end); const inclusionStart = findNonWhiteSpace( @@ -196,14 +197,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz : colonPos) + 1 ); if (preventASI) { - removeLineBreaks(code, inclusionStart, this.usedBranch!.start); + removeLineBreaks(code, inclusionStart, usedBranch!.start); } code.remove(this.start, inclusionStart); if (this.consequent.included) { code.remove(colonPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + usedBranch!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 609f0d022c2..5796281338c 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -14,19 +14,14 @@ export default class ForInStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForInStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + private deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if ( (this.left && (this.left.hasEffects(context) || @@ -48,6 +43,7 @@ export default class ForInStatement extends StatementBase { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); this.left.deoptimizePath(EMPTY_PATH); @@ -66,4 +62,9 @@ export default class ForInStatement extends StatementBase { } this.body.render(code, options); } + + private applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 65527662fc0..2b205d6069d 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -15,24 +15,20 @@ export default class ForOfStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForOfStatement; - - bind() { - this.left.bind(); - this.left.deoptimizePath(EMPTY_PATH); - this.right.bind(); - this.body.bind(); - } + private deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); // Placeholder until proper Symbol.Iterator support return true; } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); this.left.deoptimizePath(EMPTY_PATH); @@ -51,4 +47,9 @@ export default class ForOfStatement extends StatementBase { } this.body.render(code, options); } + + private applyDeoptimizations():void { + this.deoptimized = true; + this.left.deoptimizePath(EMPTY_PATH); + } } diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index b426173770d..5e6ab11ea67 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -25,6 +25,7 @@ export default class Identifier extends NodeBase implements PatternNode { variable: Variable | null = null; private bound = false; + private deoptimized = false; addExportedVariables( variables: Variable[], @@ -35,6 +36,7 @@ export default class Identifier extends NodeBase implements PatternNode { } } + // TODO Lukas get rid of bound check and remove other usages bind() { if (this.bound) return; this.bound = true; @@ -42,13 +44,6 @@ export default class Identifier extends NodeBase implements PatternNode { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); } - if ( - this.variable !== null && - this.variable instanceof LocalVariable && - this.variable.additionalInitializers !== null - ) { - this.variable.consolidateInitializers(); - } } declare(kind: string, init: ExpressionEntity) { @@ -119,6 +114,7 @@ export default class Identifier extends NodeBase implements PatternNode { } hasEffects(): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( (this.context.options.treeshake as NormalizedTreeshakingOptions).unknownGlobalSideEffects && this.variable instanceof GlobalVariable && @@ -143,6 +139,7 @@ export default class Identifier extends NodeBase implements PatternNode { } include() { + if (!this.deoptimized) this.applyDeoptimizations(); if (!this.included) { this.included = true; if (this.variable !== null) { @@ -183,6 +180,18 @@ export default class Identifier extends NodeBase implements PatternNode { } } + private applyDeoptimizations() { + this.deoptimized = true; + if ( + this.variable !== null && + this.variable instanceof LocalVariable && + this.variable.additionalInitializers !== null + ) { + this.variable.consolidateInitializers(); + } + + } + private disallowImportReassignment() { return this.context.error( { diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index d67e2db52c7..24e30930b34 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -39,12 +39,6 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable private usedBranch: ExpressionNode | null = null; private wasPathDeoptimizedWhileOptimized = false; - bind() { - super.bind(); - // ensure the usedBranch is set for the tree-shaking passes - this.getUsedBranch(); - } - deoptimizeCache() { if (this.usedBranch !== null) { this.usedBranch = null; @@ -112,7 +106,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if (this.left.hasEffects(context)) { return true; } - if (this.usedBranch !== this.left) { + if (this.getUsedBranch() !== this.left) { return this.right.hasEffects(context); } return false; @@ -120,24 +114,26 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return false; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch() + if (usedBranch === null) { return ( this.left.hasEffectsWhenAccessedAtPath(path, context) || this.right.hasEffectsWhenAccessedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAccessedAtPath(path, context); + return usedBranch.hasEffectsWhenAccessedAtPath(path, context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return true; - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch() + if (usedBranch === null) { return ( this.left.hasEffectsWhenAssignedAtPath(path, context) || this.right.hasEffectsWhenAssignedAtPath(path, context) ); } - return this.usedBranch.hasEffectsWhenAssignedAtPath(path, context); + return usedBranch.hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -145,26 +141,28 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (this.usedBranch === null) { + const usedBranch = this.getUsedBranch() + if (usedBranch === null) { return ( this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || this.right.hasEffectsWhenCalledAtPath(path, callOptions, context) ); } - return this.usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); + return usedBranch.hasEffectsWhenCalledAtPath(path, callOptions, context); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; + const usedBranch = this.getUsedBranch() if ( includeChildrenRecursively || - (this.usedBranch === this.right && this.left.shouldBeIncluded(context)) || - this.usedBranch === null + (usedBranch === this.right && this.left.shouldBeIncluded(context)) || + usedBranch === null ) { this.left.include(context, includeChildrenRecursively); this.right.include(context, includeChildrenRecursively); } else { - this.usedBranch.include(context, includeChildrenRecursively); + usedBranch.include(context, includeChildrenRecursively); } } @@ -189,7 +187,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable code.remove(operatorPos, this.end); } removeAnnotations(this, code); - this.usedBranch!.render(code, options, { + this.getUsedBranch()!.render(code, options, { isCalleeOfRenderedParent: renderedParentType ? isCalleeOfRenderedParent : (this.parent as CallExpression).callee === this, diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 070567e60ce..7a1e21897a2 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -93,6 +93,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; + // TODO Lukas get rid of bound check and other usages bind() { if (this.bound) return; this.bound = true; @@ -110,8 +111,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } else { super.bind(); - // ensure the propertyKey is set for the tree-shaking passes - this.getPropertyKey(); } } @@ -131,8 +130,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.variable) { this.variable.deoptimizePath(path); } else if (!this.replacement) { - const propertyKey = this.getPropertyKey(); - this.object.deoptimizePath([propertyKey, ...path]); + this.object.deoptimizePath([this.getPropertyKey(), ...path]); } } @@ -148,7 +146,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } else if (!this.replacement) { this.object.deoptimizeThisOnEventAtPath( event, - [this.propertyKey!, ...path], + [this.getPropertyKey(), ...path], thisParameter, recursionTracker ); @@ -210,7 +208,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE ) && propertyReadSideEffects && (propertyReadSideEffects === 'always' || - this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!], context))) + this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey()], context))) ); } @@ -222,7 +220,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.replacement) { return false; } - return this.object.hasEffectsWhenAccessedAtPath([this.propertyKey!, ...path], context); + return this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { @@ -232,7 +230,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE if (this.replacement) { return true; } - return this.object.hasEffectsWhenAssignedAtPath([this.propertyKey!, ...path], context); + return this.object.hasEffectsWhenAssignedAtPath([this.getPropertyKey(), ...path], context); } hasEffectsWhenCalledAtPath( @@ -247,7 +245,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE return true; } return this.object.hasEffectsWhenCalledAtPath( - [this.propertyKey!, ...path], + [this.getPropertyKey(), ...path], callOptions, context ); @@ -273,6 +271,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } + // TODO Lukas can we get rid of this? initialise() { this.propertyKey = getResolvablePropertyKey(this); } diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 2685f57ad97..2999784a377 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -1,26 +1,19 @@ import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { CallOptions } from '../CallOptions'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { Annotation, ExpressionNode, NodeBase } from './shared/Node'; +import { Annotation, ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class NewExpression extends NodeBase { arguments!: ExpressionNode[]; callee!: ExpressionNode; type!: NodeType.tNewExpression; - private callOptions!: CallOptions; - - bind() { - super.bind(); - for (const argument of this.arguments) { - // This will make sure all properties of parameters behave as "unknown" - argument.deoptimizePath(UNKNOWN_PATH); - } - } + private deoptimized = false; hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); for (const argument of this.arguments) { if (argument.hasEffects(context)) return true; } @@ -39,10 +32,27 @@ export default class NewExpression extends NodeBase { return path.length > 0; } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.callee.include(context, includeChildrenRecursively); + for (const argument of this.arguments) { + argument.include(context, includeChildrenRecursively); + } + } + initialise() { this.callOptions = { args: this.arguments, withNew: true }; } + + private applyDeoptimizations(): void { + this.deoptimized = true; + for (const argument of this.arguments) { + // This will make sure all properties of parameters behave as "unknown" + argument.deoptimizePath(UNKNOWN_PATH); + } + } } diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 40015a7dc02..8f9bdcc10cf 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -1,12 +1,12 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import MethodBase from './shared/MethodBase'; -import { ExpressionNode } from './shared/Node'; +import { ExpressionNode, IncludeChildren } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class Property extends MethodBase implements PatternNode { @@ -15,15 +15,8 @@ export default class Property extends MethodBase implements PatternNode { method!: boolean; shorthand!: boolean; type!: NodeType.tProperty; - private declarationInit: ExpressionEntity | null = null; - - bind() { - super.bind(); - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } + private deoptimized = false; declare(kind: string, init: ExpressionEntity) { this.declarationInit = init; @@ -31,6 +24,7 @@ export default class Property extends MethodBase implements PatternNode { } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); const propertyReadSideEffects = (this.context.options.treeshake as NormalizedTreeshakingOptions) .propertyReadSideEffects; return ( @@ -40,10 +34,25 @@ export default class Property extends MethodBase implements PatternNode { ); } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.key.include(context, includeChildrenRecursively); + this.value.include(context, includeChildrenRecursively); + } + render(code: MagicString, options: RenderOptions) { if (!this.shorthand) { this.key.render(code, options); } this.value.render(code, options, { isShorthandProperty: this.shorthand }); } + + // TODO Lukas consider making this a part of the Node interface and get rid of unneeded hasEffects and include handlers + private applyDeoptimizations():void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); + } + } } diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index b6d804dc118..66ede9c1070 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,16 +1,16 @@ -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; -import { NodeBase } from './shared/Node'; +import { IncludeChildren, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class RestElement extends NodeBase implements PatternNode { argument!: PatternNode; type!: NodeType.tRestElement; - private declarationInit: ExpressionEntity | null = null; + private deoptimized = false; addExportedVariables( variables: Variable[], @@ -19,13 +19,6 @@ export default class RestElement extends NodeBase implements PatternNode { this.argument.addExportedVariables(variables, exportNamesByVariable); } - bind() { - super.bind(); - if (this.declarationInit !== null) { - this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); - } - } - declare(kind: string, init: ExpressionEntity) { this.declarationInit = init; return this.argument.declare(kind, UNKNOWN_EXPRESSION); @@ -35,7 +28,25 @@ export default class RestElement extends NodeBase implements PatternNode { path.length === 0 && this.argument.deoptimizePath(EMPTY_PATH); } + hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); + return this.argument.hasEffects(context); + } + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument.include(context, includeChildrenRecursively); + } + + private applyDeoptimizations():void { + this.deoptimized = true; + if (this.declarationInit !== null) { + this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); + } + } } diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index cc0807079c4..c501f3ccb53 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,13 +1,26 @@ +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { argument!: ExpressionNode; type!: NodeType.tSpreadElement; + private deoptimized = false; - bind() { - super.bind(); + hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); + return this.argument.hasEffects(context); + } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument.include(context, includeChildrenRecursively); + } + + private applyDeoptimizations(): void { + this.deoptimized = true; // Only properties of properties of the argument could become subject to reassignment // This will also reassign the return values of iterators this.argument.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index c922d9323c9..63a7cc00d2a 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -14,6 +14,7 @@ export default class ThisExpression extends NodeBase { private alias!: string | null; private bound = false; + // TODO Lukas remove bound check and other usages bind() { if (this.bound) return; this.bound = true; diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index f96fdc44a0c..b7527029cf9 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,11 +1,11 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; const unaryOperators: { [operator: string]: (value: LiteralValue) => LiteralValueOrUnknown; @@ -24,13 +24,7 @@ export default class UnaryExpression extends NodeBase { operator!: '!' | '+' | '-' | 'delete' | 'typeof' | 'void' | '~'; prefix!: boolean; type!: NodeType.tUnaryExpression; - - bind() { - super.bind(); - if (this.operator === 'delete') { - this.argument.deoptimizePath(EMPTY_PATH); - } - } + private deoptimized = false; getLiteralValueAtPath( path: ObjectPath, @@ -45,6 +39,7 @@ export default class UnaryExpression extends NodeBase { } hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); if (this.operator === 'typeof' && this.argument instanceof Identifier) return false; return ( this.argument.hasEffects(context) || @@ -59,4 +54,17 @@ export default class UnaryExpression extends NodeBase { } return path.length > 1; } + + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument.include(context, includeChildrenRecursively); + } + + private applyDeoptimizations(): void { + this.deoptimized = true; + if (this.operator === 'delete') { + this.argument.deoptimizePath(EMPTY_PATH); + } + } } diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 2d2a60f621d..25db88a4b56 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -1,28 +1,21 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; import { getSystemExportFunctionLeft, getSystemExportStatement } from '../../utils/systemJsRendering'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class UpdateExpression extends NodeBase { argument!: ExpressionNode; operator!: '++' | '--'; prefix!: boolean; type!: NodeType.tUpdateExpression; - - bind() { - super.bind(); - this.argument.deoptimizePath(EMPTY_PATH); - if (this.argument instanceof Identifier) { - const variable = this.scope.findVariable(this.argument.name); - variable.isReassigned = true; - } - } + private deoptimized = false; hasEffects(context: HasEffectsContext): boolean { + if (!this.deoptimized) this.applyDeoptimizations(); return ( this.argument.hasEffects(context) || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context) @@ -33,6 +26,13 @@ export default class UpdateExpression extends NodeBase { return path.length > 1; } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument.include(context, includeChildrenRecursively); + super.include(context, includeChildrenRecursively); + } + render(code: MagicString, options: RenderOptions) { this.argument.render(code, options); if (options.format === 'system') { @@ -83,4 +83,13 @@ export default class UpdateExpression extends NodeBase { } } } + + private applyDeoptimizations() { + this.deoptimized = true; + this.argument.deoptimizePath(EMPTY_PATH); + if (this.argument instanceof Identifier) { + const variable = this.scope.findVariable(this.argument.name); + variable.isReassigned = true; + } + } } diff --git a/src/ast/nodes/YieldExpression.ts b/src/ast/nodes/YieldExpression.ts index d322951c8fd..3b4ce2d5e27 100644 --- a/src/ast/nodes/YieldExpression.ts +++ b/src/ast/nodes/YieldExpression.ts @@ -1,29 +1,30 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext } from '../ExecutionContext'; +import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { UNKNOWN_PATH } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionNode, NodeBase } from './shared/Node'; +import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class YieldExpression extends NodeBase { argument!: ExpressionNode | null; delegate!: boolean; type!: NodeType.tYieldExpression; - - bind() { - super.bind(); - if (this.argument !== null) { - this.argument.deoptimizePath(UNKNOWN_PATH); - } - } + private deoptimized = false; hasEffects(context: HasEffectsContext) { + if (!this.deoptimized) this.applyDeoptimizations(); return ( !context.ignore.returnAwaitYield || (this.argument !== null && this.argument.hasEffects(context)) ); } + include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (!this.deoptimized) this.applyDeoptimizations(); + this.included = true; + this.argument?.include(context, includeChildrenRecursively); + } + render(code: MagicString, options: RenderOptions) { if (this.argument) { this.argument.render(code, options, { preventASI: true }); @@ -32,4 +33,9 @@ export default class YieldExpression extends NodeBase { } } } + + private applyDeoptimizations() { + this.deoptimized = true; + this.argument?.deoptimizePath(UNKNOWN_PATH); + } } From 0609907e7a9cb7690c0f2c6a962e70f781b48f4d Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 May 2021 06:40:56 +0200 Subject: [PATCH 34/50] Get rid of unneeded double-binding checks --- src/ast/nodes/Identifier.ts | 8 -------- src/ast/nodes/MemberExpression.ts | 6 ------ src/ast/nodes/Super.ts | 4 ---- src/ast/nodes/ThisExpression.ts | 6 ------ 4 files changed, 24 deletions(-) diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 5e6ab11ea67..dfd1790355c 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -24,7 +24,6 @@ export default class Identifier extends NodeBase implements PatternNode { type!: NodeType.tIdentifier; variable: Variable | null = null; - private bound = false; private deoptimized = false; addExportedVariables( @@ -36,10 +35,7 @@ export default class Identifier extends NodeBase implements PatternNode { } } - // TODO Lukas get rid of bound check and remove other usages bind() { - if (this.bound) return; - this.bound = true; if (this.variable === null && isReference(this, this.parent as any)) { this.variable = this.scope.findVariable(this.name); this.variable.addReference(this); @@ -73,7 +69,6 @@ export default class Identifier extends NodeBase implements PatternNode { } deoptimizePath(path: ObjectPath) { - this.bind(); if (path.length === 0 && !this.scope.contains(this.name)) { this.disallowImportReassignment(); } @@ -86,7 +81,6 @@ export default class Identifier extends NodeBase implements PatternNode { thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - this.bind(); this.variable!.deoptimizeThisOnEventAtPath( event, path, @@ -100,7 +94,6 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - this.bind(); return this.variable!.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -109,7 +102,6 @@ export default class Identifier extends NodeBase implements PatternNode { recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - this.bind(); return this.variable!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); } diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 7a1e21897a2..d7406c68a0a 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -93,9 +93,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; - // TODO Lukas get rid of bound check and other usages bind() { - if (this.bound) return; this.bound = true; const path = getPathIfNotComputed(this); const baseVariable = path && this.scope.findVariable(path[0].key); @@ -125,7 +123,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } deoptimizePath(path: ObjectPath) { - this.bind(); if (path.length === 0) this.disallowNamespaceReassignment(); if (this.variable) { this.variable.deoptimizePath(path); @@ -140,7 +137,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE thisParameter: ExpressionEntity, recursionTracker: PathTracker ): void { - this.bind(); if (this.variable) { this.variable.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); } else if (!this.replacement) { @@ -158,7 +154,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ): LiteralValueOrUnknown { - this.bind(); if (this.variable !== null) { return this.variable.getLiteralValueAtPath(path, recursionTracker, origin); } @@ -178,7 +173,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE recursionTracker: PathTracker, origin: DeoptimizableEntity ) { - this.bind(); if (this.variable !== null) { return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); } diff --git a/src/ast/nodes/Super.ts b/src/ast/nodes/Super.ts index 19ecad28462..8a685e6b500 100644 --- a/src/ast/nodes/Super.ts +++ b/src/ast/nodes/Super.ts @@ -7,16 +7,12 @@ export default class Super extends NodeBase { type!: NodeType.tSuper; variable!: ThisVariable; - private bound = false; bind() { - if (this.bound) return; - this.bound = true; this.variable = this.scope.findVariable('this') as ThisVariable; } deoptimizePath(path: ObjectPath) { - this.bind(); this.variable.deoptimizePath(path); } diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index 63a7cc00d2a..acbeb136d32 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -12,17 +12,12 @@ export default class ThisExpression extends NodeBase { variable!: Variable; private alias!: string | null; - private bound = false; - // TODO Lukas remove bound check and other usages bind() { - if (this.bound) return; - this.bound = true; this.variable = this.scope.findVariable('this'); } deoptimizePath(path: ObjectPath) { - this.bind(); this.variable.deoptimizePath(path); } @@ -32,7 +27,6 @@ export default class ThisExpression extends NodeBase { thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - this.bind(); this.variable.deoptimizeThisOnEventAtPath( event, path, From a70e6600221aec45622235e2c595975dc511c4cf Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 May 2021 07:05:43 +0200 Subject: [PATCH 35/50] Inline deoptimizations into NodeBase for simple cases --- src/ast/nodes/ArrayExpression.ts | 28 ++++----------------------- src/ast/nodes/AssignmentExpression.ts | 5 ++--- src/ast/nodes/AssignmentPattern.ts | 20 ++++--------------- src/ast/nodes/CallExpression.ts | 5 ++--- src/ast/nodes/ForInStatement.ts | 5 ++--- src/ast/nodes/ForOfStatement.ts | 5 ++--- src/ast/nodes/Identifier.ts | 4 ++-- src/ast/nodes/ImportDeclaration.ts | 1 + src/ast/nodes/MemberExpression.ts | 6 ++---- src/ast/nodes/NewExpression.ts | 17 ++++------------ src/ast/nodes/Property.ts | 16 ++++----------- src/ast/nodes/RestElement.ts | 24 +++++++---------------- src/ast/nodes/SpreadElement.ts | 18 +++-------------- src/ast/nodes/TemplateElement.ts | 1 + src/ast/nodes/UnaryExpression.ts | 14 ++++---------- src/ast/nodes/UpdateExpression.ts | 20 ++++++++----------- src/ast/nodes/YieldExpression.ts | 14 ++++---------- src/ast/nodes/shared/Node.ts | 5 +++++ 18 files changed, 61 insertions(+), 147 deletions(-) diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index c78ab4f1131..4349913d407 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,33 +1,23 @@ import { CallOptions } from '../CallOptions'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import { arrayMembers, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled } from '../values'; import * as NodeType from './NodeType'; import { UNKNOWN_EXPRESSION } from './shared/Expression'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; // TODO Lukas turn into specific object entities export default class ArrayExpression extends NodeBase { elements!: (ExpressionNode | SpreadElement | null)[]; type!: NodeType.tArrayExpression; - private deoptimized = false; + protected deoptimized = false; getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length !== 1) return UNKNOWN_EXPRESSION; return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); } - hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - for (const element of this.elements) { - if (element && element.hasEffects(context)) { - return true; - } - } - return false; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { return path.length > 1; } @@ -43,17 +33,7 @@ export default class ArrayExpression extends NodeBase { return true; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - for (const element of this.elements) { - if (element) { - element.include(context, includeChildrenRecursively); - } - } - } - - private applyDeoptimizations():void { + protected applyDeoptimizations():void { this.deoptimized = true; for (const element of this.elements) { if (element !== null) element.deoptimizePath(UNKNOWN_PATH); diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index 4511e3e7c83..8517e48dacb 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -33,7 +33,7 @@ export default class AssignmentExpression extends NodeBase { | '**='; right!: ExpressionNode; type!: NodeType.tAssignmentExpression; - private deoptimized = false; + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); @@ -123,10 +123,9 @@ export default class AssignmentExpression extends NodeBase { } // TODO Lukas is it time for propertyWriteSideEffects? - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); - } } diff --git a/src/ast/nodes/AssignmentPattern.ts b/src/ast/nodes/AssignmentPattern.ts index 8ebd13ae251..0453a6a1008 100644 --- a/src/ast/nodes/AssignmentPattern.ts +++ b/src/ast/nodes/AssignmentPattern.ts @@ -1,19 +1,19 @@ import MagicString from 'magic-string'; import { BLANK } from '../../utils/blank'; import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { ExpressionEntity } from './shared/Expression'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class AssignmentPattern extends NodeBase implements PatternNode { left!: PatternNode; right!: ExpressionNode; type!: NodeType.tAssignmentPattern; - private deoptimized = false; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -30,22 +30,10 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { path.length === 0 && this.left.deoptimizePath(path); } - hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - return this.left.hasEffects(context) || this.right.hasEffects(context); - } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.left.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.left.include(context, includeChildrenRecursively); - this.right.include(context, includeChildrenRecursively); - } - render( code: MagicString, options: RenderOptions, @@ -55,7 +43,7 @@ export default class AssignmentPattern extends NodeBase implements PatternNode { this.right.render(code, options); } - private applyDeoptimizations():void { + protected applyDeoptimizations():void { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); this.right.deoptimizePath(UNKNOWN_PATH); diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index a693e4f5055..a056f70902e 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -42,10 +42,9 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callee!: ExpressionNode | Super; optional!: boolean; type!: NodeType.tCallExpression; - + protected deoptimized = false; private callOptions!: CallOptions; private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; - private deoptimized = false; private expressionsToBeDeoptimized = new Set(); private returnExpression: ExpressionEntity | null = null; private wasPathDeoptmizedWhileOptimized = false; @@ -303,7 +302,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; if (this.callee instanceof MemberExpression && !this.callee.variable) { this.callee.deoptimizeThisOnEventAtPath( diff --git a/src/ast/nodes/ForInStatement.ts b/src/ast/nodes/ForInStatement.ts index 5796281338c..4c92457fb19 100644 --- a/src/ast/nodes/ForInStatement.ts +++ b/src/ast/nodes/ForInStatement.ts @@ -14,7 +14,7 @@ export default class ForInStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForInStatement; - private deoptimized = false; + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); @@ -46,7 +46,6 @@ export default class ForInStatement extends StatementBase { if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -63,7 +62,7 @@ export default class ForInStatement extends StatementBase { this.body.render(code, options); } - private applyDeoptimizations():void { + protected applyDeoptimizations():void { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); } diff --git a/src/ast/nodes/ForOfStatement.ts b/src/ast/nodes/ForOfStatement.ts index 2b205d6069d..030d36ece9e 100644 --- a/src/ast/nodes/ForOfStatement.ts +++ b/src/ast/nodes/ForOfStatement.ts @@ -15,7 +15,7 @@ export default class ForOfStatement extends StatementBase { left!: VariableDeclaration | PatternNode; right!: ExpressionNode; type!: NodeType.tForOfStatement; - private deoptimized = false; + protected deoptimized = false; createScope(parentScope: Scope) { this.scope = new BlockScope(parentScope); @@ -31,7 +31,6 @@ export default class ForOfStatement extends StatementBase { if (!this.deoptimized) this.applyDeoptimizations(); this.included = true; this.left.include(context, includeChildrenRecursively || true); - this.left.deoptimizePath(EMPTY_PATH); this.right.include(context, includeChildrenRecursively); const { brokenFlow } = context; this.body.includeAsSingleStatement(context, includeChildrenRecursively); @@ -48,7 +47,7 @@ export default class ForOfStatement extends StatementBase { this.body.render(code, options); } - private applyDeoptimizations():void { + protected applyDeoptimizations(): void { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); } diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index dfd1790355c..5611f9fd603 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -24,7 +24,7 @@ export default class Identifier extends NodeBase implements PatternNode { type!: NodeType.tIdentifier; variable: Variable | null = null; - private deoptimized = false; + protected deoptimized = false; addExportedVariables( variables: Variable[], @@ -172,7 +172,7 @@ export default class Identifier extends NodeBase implements PatternNode { } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; if ( this.variable !== null && diff --git a/src/ast/nodes/ImportDeclaration.ts b/src/ast/nodes/ImportDeclaration.ts index 450381bad86..0f7d177f633 100644 --- a/src/ast/nodes/ImportDeclaration.ts +++ b/src/ast/nodes/ImportDeclaration.ts @@ -13,6 +13,7 @@ export default class ImportDeclaration extends NodeBase { specifiers!: (ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier)[]; type!: NodeType.tImportDeclaration; + // Do not bind specifiers bind() {} hasEffects() { diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index d7406c68a0a..686b3668aeb 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -87,9 +87,8 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE propertyKey!: ObjectPathKey | null; type!: NodeType.tMemberExpression; variable: Variable | null = null; - + protected deoptimized = false; private bound = false; - private deoptimized = false; private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private replacement: string | null = null; @@ -265,7 +264,6 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } - // TODO Lukas can we get rid of this? initialise() { this.propertyKey = getResolvablePropertyKey(this); } @@ -302,7 +300,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; const { propertyReadSideEffects } = this.context.options .treeshake as NormalizedTreeshakingOptions; diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 2999784a377..f65f425b46f 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -1,16 +1,16 @@ import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { CallOptions } from '../CallOptions'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { Annotation, ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { Annotation, ExpressionNode, NodeBase } from './shared/Node'; export default class NewExpression extends NodeBase { arguments!: ExpressionNode[]; callee!: ExpressionNode; type!: NodeType.tNewExpression; + protected deoptimized = false; private callOptions!: CallOptions; - private deoptimized = false; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); @@ -32,15 +32,6 @@ export default class NewExpression extends NodeBase { return path.length > 0; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.callee.include(context, includeChildrenRecursively); - for (const argument of this.arguments) { - argument.include(context, includeChildrenRecursively); - } - } - initialise() { this.callOptions = { args: this.arguments, @@ -48,7 +39,7 @@ export default class NewExpression extends NodeBase { }; } - private applyDeoptimizations(): void { + protected applyDeoptimizations(): void { this.deoptimized = true; for (const argument of this.arguments) { // This will make sure all properties of parameters behave as "unknown" diff --git a/src/ast/nodes/Property.ts b/src/ast/nodes/Property.ts index 8f9bdcc10cf..c8de9bd935d 100644 --- a/src/ast/nodes/Property.ts +++ b/src/ast/nodes/Property.ts @@ -1,12 +1,12 @@ import MagicString from 'magic-string'; import { NormalizedTreeshakingOptions } from '../../rollup/types'; import { RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; import MethodBase from './shared/MethodBase'; -import { ExpressionNode, IncludeChildren } from './shared/Node'; +import { ExpressionNode } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class Property extends MethodBase implements PatternNode { @@ -15,8 +15,8 @@ export default class Property extends MethodBase implements PatternNode { method!: boolean; shorthand!: boolean; type!: NodeType.tProperty; + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; - private deoptimized = false; declare(kind: string, init: ExpressionEntity) { this.declarationInit = init; @@ -34,13 +34,6 @@ export default class Property extends MethodBase implements PatternNode { ); } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.key.include(context, includeChildrenRecursively); - this.value.include(context, includeChildrenRecursively); - } - render(code: MagicString, options: RenderOptions) { if (!this.shorthand) { this.key.render(code, options); @@ -48,8 +41,7 @@ export default class Property extends MethodBase implements PatternNode { this.value.render(code, options, { isShorthandProperty: this.shorthand }); } - // TODO Lukas consider making this a part of the Node interface and get rid of unneeded hasEffects and include handlers - private applyDeoptimizations():void { + protected applyDeoptimizations():void { this.deoptimized = true; if (this.declarationInit !== null) { this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/RestElement.ts b/src/ast/nodes/RestElement.ts index 66ede9c1070..6ce201cdfa2 100644 --- a/src/ast/nodes/RestElement.ts +++ b/src/ast/nodes/RestElement.ts @@ -1,16 +1,17 @@ -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, UnknownKey } from '../utils/PathTracker'; +import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './shared/Expression'; -import { IncludeChildren, NodeBase } from './shared/Node'; +import { NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; export default class RestElement extends NodeBase implements PatternNode { argument!: PatternNode; type!: NodeType.tRestElement; + protected deoptimized = false; private declarationInit: ExpressionEntity | null = null; - private deoptimized = false; addExportedVariables( variables: Variable[], @@ -19,31 +20,20 @@ export default class RestElement extends NodeBase implements PatternNode { this.argument.addExportedVariables(variables, exportNamesByVariable); } - declare(kind: string, init: ExpressionEntity) { + declare(kind: string, init: ExpressionEntity): LocalVariable[] { this.declarationInit = init; return this.argument.declare(kind, UNKNOWN_EXPRESSION); } - deoptimizePath(path: ObjectPath) { + deoptimizePath(path: ObjectPath): void { path.length === 0 && this.argument.deoptimizePath(EMPTY_PATH); } - hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - return this.argument.hasEffects(context); - } - hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { return path.length > 0 || this.argument.hasEffectsWhenAssignedAtPath(EMPTY_PATH, context); } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.argument.include(context, includeChildrenRecursively); - } - - private applyDeoptimizations():void { + protected applyDeoptimizations(): void { this.deoptimized = true; if (this.declarationInit !== null) { this.declarationInit.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index c501f3ccb53..c27353f3721 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,25 +1,13 @@ -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; import { UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { argument!: ExpressionNode; type!: NodeType.tSpreadElement; - private deoptimized = false; + protected deoptimized = false; - hasEffects(context: HasEffectsContext): boolean { - if (!this.deoptimized) this.applyDeoptimizations(); - return this.argument.hasEffects(context); - } - - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.argument.include(context, includeChildrenRecursively); - } - - private applyDeoptimizations(): void { + protected applyDeoptimizations(): void { this.deoptimized = true; // Only properties of properties of the argument could become subject to reassignment // This will also reassign the return values of iterators diff --git a/src/ast/nodes/TemplateElement.ts b/src/ast/nodes/TemplateElement.ts index e5cc466fc3a..2dbe9317fe3 100644 --- a/src/ast/nodes/TemplateElement.ts +++ b/src/ast/nodes/TemplateElement.ts @@ -9,6 +9,7 @@ export default class TemplateElement extends NodeBase { raw: string; }; + // Do not try to bind value bind() {} hasEffects() { diff --git a/src/ast/nodes/UnaryExpression.ts b/src/ast/nodes/UnaryExpression.ts index b7527029cf9..11087ca408a 100644 --- a/src/ast/nodes/UnaryExpression.ts +++ b/src/ast/nodes/UnaryExpression.ts @@ -1,11 +1,11 @@ import { DeoptimizableEntity } from '../DeoptimizableEntity'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import Identifier from './Identifier'; import { LiteralValue } from './Literal'; import * as NodeType from './NodeType'; import { LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; const unaryOperators: { [operator: string]: (value: LiteralValue) => LiteralValueOrUnknown; @@ -24,7 +24,7 @@ export default class UnaryExpression extends NodeBase { operator!: '!' | '+' | '-' | 'delete' | 'typeof' | 'void' | '~'; prefix!: boolean; type!: NodeType.tUnaryExpression; - private deoptimized = false; + protected deoptimized = false; getLiteralValueAtPath( path: ObjectPath, @@ -55,13 +55,7 @@ export default class UnaryExpression extends NodeBase { return path.length > 1; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.argument.include(context, includeChildrenRecursively); - } - - private applyDeoptimizations(): void { + protected applyDeoptimizations(): void { this.deoptimized = true; if (this.operator === 'delete') { this.argument.deoptimizePath(EMPTY_PATH); diff --git a/src/ast/nodes/UpdateExpression.ts b/src/ast/nodes/UpdateExpression.ts index 25db88a4b56..b83864238c9 100644 --- a/src/ast/nodes/UpdateExpression.ts +++ b/src/ast/nodes/UpdateExpression.ts @@ -1,18 +1,21 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { getSystemExportFunctionLeft, getSystemExportStatement } from '../../utils/systemJsRendering'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { + getSystemExportFunctionLeft, + getSystemExportStatement +} from '../../utils/systemJsRendering'; +import { HasEffectsContext } from '../ExecutionContext'; import { EMPTY_PATH, ObjectPath } from '../utils/PathTracker'; import Identifier from './Identifier'; import * as NodeType from './NodeType'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; export default class UpdateExpression extends NodeBase { argument!: ExpressionNode; operator!: '++' | '--'; prefix!: boolean; type!: NodeType.tUpdateExpression; - private deoptimized = false; + protected deoptimized = false; hasEffects(context: HasEffectsContext): boolean { if (!this.deoptimized) this.applyDeoptimizations(); @@ -26,13 +29,6 @@ export default class UpdateExpression extends NodeBase { return path.length > 1; } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.argument.include(context, includeChildrenRecursively); - super.include(context, includeChildrenRecursively); - } - render(code: MagicString, options: RenderOptions) { this.argument.render(code, options); if (options.format === 'system') { @@ -84,7 +80,7 @@ export default class UpdateExpression extends NodeBase { } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; this.argument.deoptimizePath(EMPTY_PATH); if (this.argument instanceof Identifier) { diff --git a/src/ast/nodes/YieldExpression.ts b/src/ast/nodes/YieldExpression.ts index 3b4ce2d5e27..e7142d777f3 100644 --- a/src/ast/nodes/YieldExpression.ts +++ b/src/ast/nodes/YieldExpression.ts @@ -1,15 +1,15 @@ import MagicString from 'magic-string'; import { RenderOptions } from '../../utils/renderHelpers'; -import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { HasEffectsContext } from '../ExecutionContext'; import { UNKNOWN_PATH } from '../utils/PathTracker'; import * as NodeType from './NodeType'; -import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; +import { ExpressionNode, NodeBase } from './shared/Node'; export default class YieldExpression extends NodeBase { argument!: ExpressionNode | null; delegate!: boolean; type!: NodeType.tYieldExpression; - private deoptimized = false; + protected deoptimized = false; hasEffects(context: HasEffectsContext) { if (!this.deoptimized) this.applyDeoptimizations(); @@ -19,12 +19,6 @@ export default class YieldExpression extends NodeBase { ); } - include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { - if (!this.deoptimized) this.applyDeoptimizations(); - this.included = true; - this.argument?.include(context, includeChildrenRecursively); - } - render(code: MagicString, options: RenderOptions) { if (this.argument) { this.argument.render(code, options, { preventASI: true }); @@ -34,7 +28,7 @@ export default class YieldExpression extends NodeBase { } } - private applyDeoptimizations() { + protected applyDeoptimizations() { this.deoptimized = true; this.argument?.deoptimizePath(UNKNOWN_PATH); } diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 5f8ba91ef01..e4ff1f20c3a 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -96,6 +96,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { scope!: ChildScope; start!: number; type!: keyof typeof NodeType; + protected deoptimized?: boolean; constructor( esTreeNode: GenericEsTreeNode, @@ -145,6 +146,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } hasEffects(context: HasEffectsContext): boolean { + if (this.deoptimized === false) this.applyDeoptimizations(); for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; if (value === null) continue; @@ -158,6 +160,7 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { + if (this.deoptimized === false) this.applyDeoptimizations(); this.included = true; for (const key of this.keys) { const value = (this as GenericEsTreeNode)[key]; @@ -230,6 +233,8 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { shouldBeIncluded(context: InclusionContext): boolean { return this.included || (!context.brokenFlow && this.hasEffects(createHasEffectsContext())); } + + protected applyDeoptimizations(): void {} } export { NodeBase as StatementBase }; From ea5fe1d2849d62a278c83eb15520a1eccb024332 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 10 May 2021 21:22:43 +0200 Subject: [PATCH 36/50] Add more efficient way to create object entities --- src/ast/nodes/shared/FunctionNode.ts | 5 ++-- src/ast/nodes/shared/Node.ts | 4 +++ src/ast/nodes/shared/ObjectEntity.ts | 25 ++++++++++++---- src/ast/nodes/shared/ObjectPrototype.ts | 16 +++++----- src/ast/values.ts | 40 ++----------------------- 5 files changed, 37 insertions(+), 53 deletions(-) diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index dfbbcedc3bb..57085a101b0 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -2,13 +2,14 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import FunctionScope from '../../scopes/FunctionScope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { UnknownObjectExpression } from '../../values'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; import { EVENT_CALLED, ExpressionEntity, NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; +import { ObjectEntity } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import { PatternNode } from './Pattern'; // TODO Lukas improve prototype handling to fix #2219 @@ -83,7 +84,7 @@ export default class FunctionNode extends NodeBase { const thisInit = context.replacedVariableInits.get(this.scope.thisVariable); context.replacedVariableInits.set( this.scope.thisVariable, - callOptions.withNew ? new UnknownObjectExpression() : UNKNOWN_EXPRESSION + callOptions.withNew ? new ObjectEntity({}, OBJECT_PROTOTYPE) : UNKNOWN_EXPRESSION ); const { brokenFlow, ignore } = context; context.ignore = { diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index e4ff1f20c3a..72ce839432b 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -96,6 +96,10 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { scope!: ChildScope; start!: number; type!: keyof typeof NodeType; + // Nodes can apply custom deoptimizations once they become part of the + // executed code. To do this, they must initialize this as false, implement + // applyDeoptimizations and call this from include and hasEffects if they + // have custom handlers protected deoptimized?: boolean; constructor( diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 8db18c5c31a..25e35ef9f43 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -26,7 +26,6 @@ export interface ObjectProperty { type PropertyMap = Record; -// TODO Lukas add a way to directly inject only propertiesByKey and create allProperties lazily/not export class ObjectEntity extends ExpressionEntity { private readonly allProperties: ExpressionEntity[] = []; private readonly deoptimizedPaths: Record = Object.create(null); @@ -44,9 +43,21 @@ export class ObjectEntity extends ExpressionEntity { private readonly unmatchablePropertiesAndGetters: ExpressionEntity[] = []; private readonly unmatchableSetters: ExpressionEntity[] = []; - constructor(properties: ObjectProperty[], private prototypeExpression: ExpressionEntity | null) { + // If a PropertyMap is used, this will be taken as propertiesAndGettersByKey + // and we assume there are no setters or getters + constructor( + properties: ObjectProperty[] | PropertyMap, + private prototypeExpression: ExpressionEntity | null + ) { super(); - this.buildPropertyMaps(properties); + if (Array.isArray(properties)) { + this.buildPropertyMaps(properties); + } else { + this.propertiesAndGettersByKey = this.propertiesAndSettersByKey = properties; + for (const propertiesForKey of Object.values(properties)) { + this.allProperties.push(...propertiesForKey); + } + } } deoptimizeAllProperties(): void { @@ -54,8 +65,12 @@ export class ObjectEntity extends ExpressionEntity { return; } this.hasUnknownDeoptimizedProperty = true; - for (const property of this.allProperties) { - property.deoptimizePath(UNKNOWN_PATH); + for (const properties of Object.values(this.propertiesAndGettersByKey).concat( + Object.values(this.settersByKey) + )) { + for (const property of properties) { + property.deoptimizePath(UNKNOWN_PATH); + } } // While the prototype itself cannot be mutated, each property can this.prototypeExpression?.deoptimizePath([UnknownKey, UnknownKey]); diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts index 03fdd8fe48c..d3a12b0468c 100644 --- a/src/ast/nodes/shared/ObjectPrototype.ts +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -6,13 +6,13 @@ import { import { ObjectEntity } from './ObjectEntity'; export const OBJECT_PROTOTYPE = new ObjectEntity( - [ - { key: 'hasOwnProperty', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, - { key: 'isPrototypeOf', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, - { key: 'propertyIsEnumerable', kind: 'init', property: METHOD_RETURNS_BOOLEAN }, - { key: 'toLocaleString', kind: 'init', property: METHOD_RETURNS_STRING }, - { key: 'toString', kind: 'init', property: METHOD_RETURNS_STRING }, - { key: 'valueOf', kind: 'init', property: METHOD_RETURNS_UNKNOWN } - ], + { + hasOwnProperty: [METHOD_RETURNS_BOOLEAN], + isPrototypeOf: [METHOD_RETURNS_BOOLEAN], + propertyIsEnumerable: [METHOD_RETURNS_BOOLEAN], + toLocaleString: [METHOD_RETURNS_STRING], + toString: [METHOD_RETURNS_STRING], + valueOf: [METHOD_RETURNS_UNKNOWN] + }, null ); diff --git a/src/ast/values.ts b/src/ast/values.ts index bc3228a5484..b0477a736be 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -231,44 +231,7 @@ const returnsString: RawMemberDescription = { } }; -// TODO Lukas This could just be the constant and above, we use an ObjectEntity with OBJECT_PROTOTYPE as prototype -// TODO Lukas Also, the name should reflect we assume neither getters nor setters and do not override builtins -// or just remove? -export class UnknownObjectExpression extends ExpressionEntity { - included = false; - - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(objectMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (path.length === 1) { - return hasMemberEffectWhenCalled(objectMembers, path[0], this.included, callOptions, context); - } - return true; - } - - include() { - this.included = true; - } -} - -export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ +const objectMembers: MemberDescriptions = assembleMemberDescriptions({ hasOwnProperty: returnsBoolean, isPrototypeOf: returnsBoolean, propertyIsEnumerable: returnsBoolean, @@ -277,6 +240,7 @@ export const objectMembers: MemberDescriptions = assembleMemberDescriptions({ valueOf: returnsUnknown }); +// TODO Lukas first make arrays proper objects, then get rid of this export const arrayMembers: MemberDescriptions = assembleMemberDescriptions( { concat: returnsArray, From 02c11278c270211a07b90279f8c4a6b9ea953f29 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 12 May 2021 06:17:09 +0200 Subject: [PATCH 37/50] Add thisParameter to CallOptions --- src/ast/CallOptions.ts | 1 + src/ast/nodes/CallExpression.ts | 48 ++++++++++++----------- src/ast/nodes/NewExpression.ts | 2 + src/ast/nodes/TaggedTemplateExpression.ts | 2 + src/ast/nodes/shared/Expression.ts | 13 ++++++ src/ast/nodes/shared/MethodBase.ts | 2 + src/ast/nodes/shared/MethodTypes.ts | 2 + src/ast/values.ts | 2 + src/ast/variables/LocalVariable.ts | 21 +++++----- 9 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/ast/CallOptions.ts b/src/ast/CallOptions.ts index ad368d69951..bff7691ea20 100644 --- a/src/ast/CallOptions.ts +++ b/src/ast/CallOptions.ts @@ -5,5 +5,6 @@ export const NO_ARGS = []; export interface CallOptions { args: (ExpressionEntity | SpreadElement)[]; + thisParam: ExpressionEntity | null; withNew: boolean; } diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index a056f70902e..f4209ba8504 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -75,6 +75,14 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt ); } } + this.callOptions = { + args: this.arguments, + thisParam: + this.callee instanceof MemberExpression && !this.callee.variable + ? this.callee.object + : null, + withNew: false + }; } deoptimizeCache() { @@ -116,6 +124,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } + // TODO Lukas allow "event" to be a CallOptions object and get rid of thisParameter deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, @@ -200,18 +209,21 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0 || context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) { + if (path.length === 0) { return false; } - return this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context); + return ( + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) + ); } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return true; - if (context.assigned.trackEntityAtPathAndGetIfTracked(path, this)) { - return false; - } - return this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context); + return ( + !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && + this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) + ); } hasEffectsWhenCalledAtPath( @@ -219,15 +231,13 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt callOptions: CallOptions, context: HasEffectsContext ): boolean { - if ( - (callOptions.withNew + return ( + !(callOptions.withNew ? context.instantiated : context.called - ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) - ) { - return false; - } - return this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context); + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.getReturnExpression().hasEffectsWhenCalledAtPath(path, callOptions, context) + ); } include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { @@ -252,13 +262,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - initialise() { - this.callOptions = { - args: this.arguments, - withNew: false - }; - } - render( code: MagicString, options: RenderOptions, @@ -304,11 +307,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt protected applyDeoptimizations() { this.deoptimized = true; - if (this.callee instanceof MemberExpression && !this.callee.variable) { + const { thisParam } = this.callOptions; + if (thisParam) { this.callee.deoptimizeThisOnEventAtPath( EVENT_CALLED, EMPTY_PATH, - this.callee.object, + thisParam, SHARED_RECURSION_TRACKER ); } diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index f65f425b46f..54626a493c1 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -35,6 +35,8 @@ export default class NewExpression extends NodeBase { initialise() { this.callOptions = { args: this.arguments, + // TODO Lukas does it make sense, to use a pre-made object expression here? Can we get rid of "withNew"? + thisParam: null, withNew: true }; } diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index 84e9dffc3fe..ef830dcfd5f 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -52,6 +52,8 @@ export default class TaggedTemplateExpression extends NodeBase { initialise() { this.callOptions = { args: NO_ARGS, + // TODO Lukas verify + thisParam: null, withNew: false, }; } diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index d6711a3bfdd..87d5315c2d5 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -3,7 +3,11 @@ import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; +import AssignmentExpression from '../AssignmentExpression'; +import CallExpression from '../CallExpression'; import { LiteralValue } from '../Literal'; +import MemberExpression from '../MemberExpression'; +import NewExpression from '../NewExpression'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; @@ -16,11 +20,20 @@ export const EVENT_ASSIGNED = 1; export const EVENT_CALLED = 2; export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; +// TODO Lukas use this for events? +interface NodeEventOptions { + event: NodeEvent; + initiator: CallExpression | NewExpression | AssignmentExpression | MemberExpression; + parameters: ExpressionEntity[]; + thisParameter: ExpressionEntity | null; +} + export class ExpressionEntity implements WritableEntity { included = false; deoptimizePath(_path: ObjectPath): void {} + // TODO Lukas this should become "deoptimizeParamsOnEventAtPath" with a "NodeEventOptions" instance as parameter? deoptimizeThisOnEventAtPath( _event: NodeEvent, _path: ObjectPath, diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index bb9f3f0dd5f..ce203945e10 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -24,6 +24,8 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity private accessedValue: ExpressionEntity | null = null; private accessorCallOptions: CallOptions = { args: NO_ARGS, + // TODO Lukas in the end, handle this differently or get rid of the shared call options + thisParam: null, withNew: false }; diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index 6f684d06dc3..fc23ac3ac93 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -63,6 +63,8 @@ class Method extends ExpressionEntity { EMPTY_PATH, { args: NO_ARGS, + // TODO Lukas check if we need something else here + thisParam: null, withNew: false }, context diff --git a/src/ast/values.ts b/src/ast/values.ts index b0477a736be..ee65bc908d9 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -363,6 +363,8 @@ export function hasMemberEffectWhenCalled( EMPTY_PATH, { args: NO_ARGS, + // TODO Lukas think about this + thisParam: null, withNew: false }, context diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 02912d713e7..a183175b17b 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -149,16 +149,18 @@ export default class LocalVariable extends Variable { hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { if (path.length === 0) return false; if (this.isReassigned || path.length > MAX_PATH_DEPTH) return true; - if (context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) return false; - return (this.init && this.init.hasEffectsWhenAccessedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAccessedAtPath(path, context))!; } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { if (this.included || path.length > MAX_PATH_DEPTH) return true; if (path.length === 0) return false; if (this.isReassigned) return true; - if (context.accessed.trackEntityAtPathAndGetIfTracked(path, this)) return false; - return (this.init && this.init.hasEffectsWhenAssignedAtPath(path, context))!; + return (this.init && + !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && + this.init.hasEffectsWhenAssignedAtPath(path, context))!; } hasEffectsWhenCalledAtPath( @@ -167,15 +169,12 @@ export default class LocalVariable extends Variable { context: HasEffectsContext ) { if (path.length > MAX_PATH_DEPTH || this.isReassigned) return true; - if ( - (callOptions.withNew + return (this.init && + !(callOptions.withNew ? context.instantiated : context.called - ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) - ) { - return false; - } - return (this.init && this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; + ).trackEntityAtPathAndGetIfTracked(path, callOptions, this) && + this.init.hasEffectsWhenCalledAtPath(path, callOptions, context))!; } include() { From ab15b6015b1861fbe0e0ffddb06ea30daff2b612 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 12 May 2021 06:37:26 +0200 Subject: [PATCH 38/50] Move NodeEvents to separate file --- src/ast/NodeEvents.ts | 4 ++++ src/ast/nodes/CallExpression.ts | 3 +-- src/ast/nodes/ConditionalExpression.ts | 3 +-- src/ast/nodes/Identifier.ts | 3 ++- src/ast/nodes/LogicalExpression.ts | 3 ++- src/ast/nodes/MemberExpression.ts | 4 +--- src/ast/nodes/ObjectExpression.ts | 2 +- src/ast/nodes/PropertyDefinition.ts | 2 +- src/ast/nodes/SequenceExpression.ts | 3 ++- src/ast/nodes/ThisExpression.ts | 3 ++- src/ast/nodes/shared/ClassNode.ts | 3 ++- src/ast/nodes/shared/Expression.ts | 19 +------------------ src/ast/nodes/shared/FunctionNode.ts | 3 ++- src/ast/nodes/shared/MethodBase.ts | 5 +---- src/ast/nodes/shared/MethodTypes.ts | 3 ++- src/ast/nodes/shared/ObjectEntity.ts | 4 +--- src/ast/nodes/shared/ObjectMember.ts | 3 ++- src/ast/variables/LocalVariable.ts | 2 +- src/ast/variables/ThisVariable.ts | 2 +- 19 files changed, 30 insertions(+), 44 deletions(-) create mode 100644 src/ast/NodeEvents.ts diff --git a/src/ast/NodeEvents.ts b/src/ast/NodeEvents.ts new file mode 100644 index 00000000000..e952677032a --- /dev/null +++ b/src/ast/NodeEvents.ts @@ -0,0 +1,4 @@ +export const EVENT_ACCESSED = 0; +export const EVENT_ASSIGNED = 1; +export const EVENT_CALLED = 2; +export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index f4209ba8504..2ea1dfb7cbf 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -9,6 +9,7 @@ import { import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -20,10 +21,8 @@ import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; import * as NodeType from './NodeType'; import { - EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 4dd5524b26e..f6b54205a98 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -21,10 +22,8 @@ import { import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; import { - EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 5611f9fd603..f2f2171575f 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -6,13 +6,14 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import FunctionScope from '../scopes/FunctionScope'; import { EMPTY_PATH, ObjectPath, PathTracker } from '../utils/PathTracker'; import GlobalVariable from '../variables/GlobalVariable'; import LocalVariable from '../variables/LocalVariable'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; import { PatternNode } from './shared/Pattern'; import SpreadElement from './SpreadElement'; diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 24e30930b34..acc3d2f9165 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,6 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -20,7 +21,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent, UnknownValue } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 686b3668aeb..3582458579f 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -6,6 +6,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -24,11 +25,8 @@ import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; import { - EVENT_ACCESSED, - EVENT_ASSIGNED, ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 300678e4081..15ee0012024 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -4,6 +4,7 @@ import { NodeRenderOptions, RenderOptions } from '../../utils/renderHelpers'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; import Identifier from './Identifier'; import Literal from './Literal'; @@ -12,7 +13,6 @@ import Property from './Property'; import { ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 968fb787dbc..0a5e2656caf 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -1,13 +1,13 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; import { ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; diff --git a/src/ast/nodes/SequenceExpression.ts b/src/ast/nodes/SequenceExpression.ts index 0e5c4d87695..cc3b234ac76 100644 --- a/src/ast/nodes/SequenceExpression.ts +++ b/src/ast/nodes/SequenceExpression.ts @@ -10,10 +10,11 @@ import { treeshakeNode } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; export default class SequenceExpression extends NodeBase { diff --git a/src/ast/nodes/ThisExpression.ts b/src/ast/nodes/ThisExpression.ts index acbeb136d32..b1f2ada38aa 100644 --- a/src/ast/nodes/ThisExpression.ts +++ b/src/ast/nodes/ThisExpression.ts @@ -1,10 +1,11 @@ import MagicString from 'magic-string'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ModuleScope from '../scopes/ModuleScope'; import { ObjectPath, PathTracker } from '../utils/PathTracker'; import Variable from '../variables/Variable'; import * as NodeType from './NodeType'; -import { ExpressionEntity, NodeEvent } from './shared/Expression'; +import { ExpressionEntity } from './shared/Expression'; import { NodeBase } from './shared/Node'; export default class ThisExpression extends NodeBase { diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index a17e09b6c1a..3c8b7fcd1d3 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -1,6 +1,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; import ChildScope from '../../scopes/ChildScope'; import Scope from '../../scopes/Scope'; import { @@ -14,7 +15,7 @@ import ClassBody from '../ClassBody'; import Identifier from '../Identifier'; import Literal from '../Literal'; import MethodDefinition from '../MethodDefinition'; -import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent, UnknownValue } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { ObjectEntity, ObjectProperty } from './ObjectEntity'; import { ObjectMember } from './ObjectMember'; diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 87d5315c2d5..55ecbc867a6 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -2,12 +2,9 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { WritableEntity } from '../../Entity'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; import { ObjectPath, PathTracker, UNKNOWN_PATH } from '../../utils/PathTracker'; -import AssignmentExpression from '../AssignmentExpression'; -import CallExpression from '../CallExpression'; import { LiteralValue } from '../Literal'; -import MemberExpression from '../MemberExpression'; -import NewExpression from '../NewExpression'; import SpreadElement from '../SpreadElement'; import { ExpressionNode, IncludeChildren } from './Node'; @@ -15,25 +12,11 @@ export const UnknownValue = Symbol('Unknown Value'); export type LiteralValueOrUnknown = LiteralValue | typeof UnknownValue; -export const EVENT_ACCESSED = 0; -export const EVENT_ASSIGNED = 1; -export const EVENT_CALLED = 2; -export type NodeEvent = typeof EVENT_ACCESSED | typeof EVENT_ASSIGNED | typeof EVENT_CALLED; - -// TODO Lukas use this for events? -interface NodeEventOptions { - event: NodeEvent; - initiator: CallExpression | NewExpression | AssignmentExpression | MemberExpression; - parameters: ExpressionEntity[]; - thisParameter: ExpressionEntity | null; -} - export class ExpressionEntity implements WritableEntity { included = false; deoptimizePath(_path: ObjectPath): void {} - // TODO Lukas this should become "deoptimizeParamsOnEventAtPath" with a "NodeEventOptions" instance as parameter? deoptimizeThisOnEventAtPath( _event: NodeEvent, _path: ObjectPath, diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 57085a101b0..a61462f9ba1 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -1,12 +1,13 @@ import { CallOptions } from '../../CallOptions'; import { BROKEN_FLOW_NONE, HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import FunctionScope from '../../scopes/FunctionScope'; import { ObjectPath, UnknownKey, UNKNOWN_PATH } from '../../utils/PathTracker'; import BlockStatement from '../BlockStatement'; import Identifier, { IdentifierWithVariable } from '../Identifier'; import RestElement from '../RestElement'; import SpreadElement from '../SpreadElement'; -import { EVENT_CALLED, ExpressionEntity, NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, GenericEsTreeNode, IncludeChildren, NodeBase } from './Node'; import { ObjectEntity } from './ObjectEntity'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index ce203945e10..fb2f9cba34c 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -1,15 +1,12 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; import PrivateIdentifier from '../PrivateIdentifier'; import { - EVENT_ACCESSED, - EVENT_ASSIGNED, - EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index fc23ac3ac93..9a0dc147361 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,9 +1,10 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; +import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../../utils/PathTracker'; import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_STRING } from '../../values'; import SpreadElement from '../SpreadElement'; -import { EVENT_CALLED, ExpressionEntity, NodeEvent, UNKNOWN_EXPRESSION } from './Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode } from './Node'; type MethodDescription = { diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 25e35ef9f43..90f898685ab 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -1,6 +1,7 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { EVENT_ACCESSED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; import { ObjectPath, ObjectPathKey, @@ -9,11 +10,8 @@ import { UNKNOWN_PATH } from '../../utils/PathTracker'; import { - EVENT_ACCESSED, - EVENT_CALLED, ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from './Expression'; diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index 9863182254e..b91c168a2ca 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -1,8 +1,9 @@ import { CallOptions } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; +import { NodeEvent } from '../../NodeEvents'; import { ObjectPath, PathTracker } from '../../utils/PathTracker'; -import { ExpressionEntity, LiteralValueOrUnknown, NodeEvent } from './Expression'; +import { ExpressionEntity, LiteralValueOrUnknown } from './Expression'; export class ObjectMember extends ExpressionEntity { constructor(private readonly object: ExpressionEntity, private readonly key: string) { diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index a183175b17b..8981269ff73 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -2,13 +2,13 @@ import Module, { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { createInclusionContext, HasEffectsContext, InclusionContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import ExportDefaultDeclaration from '../nodes/ExportDefaultDeclaration'; import Identifier from '../nodes/Identifier'; import * as NodeType from '../nodes/NodeType'; import { ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 323e3ea1293..5ca668c454d 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,10 +1,10 @@ import { AstContext } from '../../Module'; import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; +import { NodeEvent } from '../NodeEvents'; import { ExpressionEntity, LiteralValueOrUnknown, - NodeEvent, UnknownValue, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; From ece8f31b500d3ee546624477cbe82760a9a5a3b7 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 14 May 2021 16:32:11 +0200 Subject: [PATCH 39/50] Track array elements --- src/ast/nodes/ArrayExpression.ts | 92 +++++++-- src/ast/nodes/CallExpression.ts | 12 +- src/ast/nodes/ConditionalExpression.ts | 34 ++-- src/ast/nodes/Identifier.ts | 18 +- src/ast/nodes/LogicalExpression.ts | 22 ++- src/ast/nodes/MemberExpression.ts | 11 +- src/ast/nodes/ObjectExpression.ts | 11 +- src/ast/nodes/PropertyDefinition.ts | 3 +- src/ast/nodes/SpreadElement.ts | 10 +- src/ast/nodes/shared/ArrayPrototype.ts | 144 ++++++++++++++ src/ast/nodes/shared/ClassNode.ts | 7 +- src/ast/nodes/shared/Expression.ts | 1 + src/ast/nodes/shared/MethodBase.ts | 16 +- src/ast/nodes/shared/MethodTypes.ts | 83 +++++--- src/ast/nodes/shared/MultiExpression.ts | 3 +- src/ast/nodes/shared/ObjectEntity.ts | 137 +++++++++++--- src/ast/nodes/shared/ObjectMember.ts | 2 + src/ast/nodes/shared/ObjectPrototype.ts | 20 +- src/ast/utils/PathTracker.ts | 6 +- src/ast/values.ts | 132 +------------ src/ast/variables/LocalVariable.ts | 8 +- .../array-elements/_config.js | 3 + .../array-elements/_expected.js | 4 + .../array-elements/main.js | 10 + .../array-mutation/_config.js | 3 + .../array-mutation/_expected.js | 10 + .../array-mutation/main.js | 11 ++ .../array-spread/_config.js | 3 + .../array-spread/_expected.js | 9 + .../array-spread/main.js | 11 ++ .../spread-element-deoptimization/_config.js | 3 + .../_expected.js | 3 + .../spread-element-deoptimization/main.js | 3 + .../array-expression/_expected.js | 87 +++++++-- .../array-expression/main.js | 179 ++++++++++++------ .../builtin-prototypes/literal/main.js | 2 +- test/function/samples/array-getter/_config.js | 3 + test/function/samples/array-getter/main.js | 14 ++ 38 files changed, 793 insertions(+), 337 deletions(-) create mode 100644 src/ast/nodes/shared/ArrayPrototype.ts create mode 100644 test/form/samples/array-element-tracking/array-elements/_config.js create mode 100644 test/form/samples/array-element-tracking/array-elements/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-elements/main.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/_config.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-mutation/main.js create mode 100644 test/form/samples/array-element-tracking/array-spread/_config.js create mode 100644 test/form/samples/array-element-tracking/array-spread/_expected.js create mode 100644 test/form/samples/array-element-tracking/array-spread/main.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js create mode 100644 test/form/samples/array-element-tracking/spread-element-deoptimization/main.js create mode 100644 test/function/samples/array-getter/_config.js create mode 100644 test/function/samples/array-getter/main.js diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index 4349913d407..db560d8ee52 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -1,25 +1,71 @@ import { CallOptions } from '../CallOptions'; +import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; -import { arrayMembers, getMemberReturnExpressionWhenCalled, hasMemberEffectWhenCalled } from '../values'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownInteger } from '../utils/PathTracker'; +import { UNDEFINED_EXPRESSION, UNKNOWN_LITERAL_NUMBER } from '../values'; import * as NodeType from './NodeType'; -import { UNKNOWN_EXPRESSION } from './shared/Expression'; +import { ARRAY_PROTOTYPE } from './shared/ArrayPrototype'; +import { ExpressionEntity, LiteralValueOrUnknown } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; +import { ObjectEntity, ObjectProperty } from './shared/ObjectEntity'; import SpreadElement from './SpreadElement'; -// TODO Lukas turn into specific object entities export default class ArrayExpression extends NodeBase { elements!: (ExpressionNode | SpreadElement | null)[]; type!: NodeType.tArrayExpression; - protected deoptimized = false; + private objectEntity: ObjectEntity | null = null; - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length !== 1) return UNKNOWN_EXPRESSION; - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); + deoptimizeCache() { + this.getObjectEntity().deoptimizeAllProperties(); } - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; + deoptimizePath(path: ObjectPath) { + this.getObjectEntity().deoptimizePath(path); + } + + deoptimizeThisOnEventAtPath( + event: NodeEvent, + path: ObjectPath, + thisParameter: ExpressionEntity, + recursionTracker: PathTracker + ) { + 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 { + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); + } + + hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAccessedAtPath(path, context); + } + + hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext) { + return this.getObjectEntity().hasEffectsWhenAssignedAtPath(path, context); } hasEffectsWhenCalledAtPath( @@ -27,16 +73,26 @@ export default class ArrayExpression extends NodeBase { callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); - } - return true; + return this.getObjectEntity().hasEffectsWhenCalledAtPath(path, callOptions, context); } - protected applyDeoptimizations():void { - this.deoptimized = true; - for (const element of this.elements) { - if (element !== null) element.deoptimizePath(UNKNOWN_PATH); + private getObjectEntity(): ObjectEntity { + if (this.objectEntity !== null) { + return this.objectEntity; + } + const properties: ObjectProperty[] = [ + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } + ]; + for (let index = 0; index < this.elements.length; index++) { + const element = this.elements[index]; + if (element instanceof SpreadElement) { + properties.unshift({ kind: 'init', key: UnknownInteger, property: element }); + } else if (!element) { + properties.push({ kind: 'init', key: String(index), property: UNDEFINED_EXPRESSION }); + } else { + properties.push({ kind: 'init', key: String(index), property: element }); + } } + return (this.objectEntity = new ObjectEntity(properties, ARRAY_PROTOTYPE)); } } diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index 2ea1dfb7cbf..ee6304f4396 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -88,8 +88,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt if (this.returnExpression !== UNKNOWN_EXPRESSION) { this.returnExpression = null; const returnExpression = this.getReturnExpression(); - const deoptimizableDependentExpressions = this.deoptimizableDependentExpressions; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; + const { deoptimizableDependentExpressions, expressionsToBeDeoptimized } = this; if (returnExpression !== UNKNOWN_EXPRESSION) { // We need to replace here because it is possible new expressions are added // while we are deoptimizing the old ones @@ -173,6 +172,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -185,7 +185,12 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt returnExpression, () => { this.deoptimizableDependentExpressions.push(origin); - return returnExpression.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return returnExpression.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); }, UNKNOWN_EXPRESSION ); @@ -328,6 +333,7 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt this.returnExpression = UNKNOWN_EXPRESSION; return (this.returnExpression = this.callee.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, + this.callOptions, recursionTracker, this )); diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index f6b54205a98..8258283c2ff 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -21,11 +21,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { - ExpressionEntity, - LiteralValueOrUnknown, - UnknownValue -} from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; @@ -94,17 +90,33 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.consequent.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.alternate.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.consequent.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ), + this.alternate.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { @@ -158,11 +170,7 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; const usedBranch = this.getUsedBranch(); - if ( - includeChildrenRecursively || - this.test.shouldBeIncluded(context) || - usedBranch === null - ) { + if (includeChildrenRecursively || this.test.shouldBeIncluded(context) || usedBranch === null) { this.test.include(context, includeChildrenRecursively); this.consequent.include(context, includeChildrenRecursively); this.alternate.include(context, includeChildrenRecursively); diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index f2f2171575f..6ce39e6cbdb 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -82,12 +82,7 @@ export default class Identifier extends NodeBase implements PatternNode { thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - this.variable!.deoptimizeThisOnEventAtPath( - event, - path, - thisParameter, - recursionTracker - ); + this.variable!.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); } getLiteralValueAtPath( @@ -100,10 +95,16 @@ export default class Identifier extends NodeBase implements PatternNode { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { - return this.variable!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + ): ExpressionEntity { + return this.variable!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(): boolean { @@ -182,7 +183,6 @@ export default class Identifier extends NodeBase implements PatternNode { ) { this.variable.consolidateInitializers(); } - } private disallowImportReassignment() { diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index acc3d2f9165..385861a406c 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -21,7 +21,7 @@ import { } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; -import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; import { MultiExpression } from './shared/MultiExpression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; @@ -90,17 +90,23 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { const usedBranch = this.getUsedBranch(); if (usedBranch === null) return new MultiExpression([ - this.left.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin), - this.right.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + this.left.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin), + this.right.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ]); this.expressionsToBeDeoptimized.push(origin); - return usedBranch.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return usedBranch.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } hasEffects(context: HasEffectsContext): boolean { @@ -115,7 +121,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return false; - const usedBranch = this.getUsedBranch() + const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( this.left.hasEffectsWhenAccessedAtPath(path, context) || @@ -127,7 +133,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { if (path.length === 0) return true; - const usedBranch = this.getUsedBranch() + const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( this.left.hasEffectsWhenAssignedAtPath(path, context) || @@ -142,7 +148,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable callOptions: CallOptions, context: HasEffectsContext ): boolean { - const usedBranch = this.getUsedBranch() + const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( this.left.hasEffectsWhenCalledAtPath(path, callOptions, context) || @@ -154,7 +160,7 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable include(context: InclusionContext, includeChildrenRecursively: IncludeChildren) { this.included = true; - const usedBranch = this.getUsedBranch() + const usedBranch = this.getUsedBranch(); if ( includeChildrenRecursively || (usedBranch === this.right && this.left.shouldBeIncluded(context)) || diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index 3582458579f..cb77210c11c 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -167,11 +167,17 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity - ) { + ): ExpressionEntity { if (this.variable !== null) { - return this.variable.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return this.variable.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); } if (this.replacement) { return UNKNOWN_EXPRESSION; @@ -179,6 +185,7 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE this.expressionsToBeDeoptimized.push(origin); return this.object.getReturnExpressionWhenCalledAtPath( [this.getPropertyKey(), ...path], + callOptions, recursionTracker, origin ); diff --git a/src/ast/nodes/ObjectExpression.ts b/src/ast/nodes/ObjectExpression.ts index 15ee0012024..561ef146cf8 100644 --- a/src/ast/nodes/ObjectExpression.ts +++ b/src/ast/nodes/ObjectExpression.ts @@ -5,7 +5,13 @@ import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext } from '../ExecutionContext'; import { NodeEvent } from '../NodeEvents'; -import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UnknownKey } from '../utils/PathTracker'; +import { + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER, + UnknownKey +} from '../utils/PathTracker'; import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; @@ -24,7 +30,6 @@ import SpreadElement from './SpreadElement'; export default class ObjectExpression extends NodeBase implements DeoptimizableEntity { properties!: (Property | SpreadElement)[]; type!: NodeType.tObjectExpression; - private objectEntity: ObjectEntity | null = null; deoptimizeCache() { @@ -59,11 +64,13 @@ export default class ObjectExpression extends NodeBase implements DeoptimizableE getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( path, + callOptions, recursionTracker, origin ); diff --git a/src/ast/nodes/PropertyDefinition.ts b/src/ast/nodes/PropertyDefinition.ts index 0a5e2656caf..92dcd0eec02 100644 --- a/src/ast/nodes/PropertyDefinition.ts +++ b/src/ast/nodes/PropertyDefinition.ts @@ -45,11 +45,12 @@ export default class PropertyDefinition extends NodeBase { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.value - ? this.value.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + ? this.value.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) : UNKNOWN_EXPRESSION; } diff --git a/src/ast/nodes/SpreadElement.ts b/src/ast/nodes/SpreadElement.ts index c27353f3721..a8246dddbdc 100644 --- a/src/ast/nodes/SpreadElement.ts +++ b/src/ast/nodes/SpreadElement.ts @@ -1,5 +1,7 @@ -import { UnknownKey } from '../utils/PathTracker'; +import { NodeEvent } from '../NodeEvents'; +import { ObjectPath, PathTracker, UnknownKey } from '../utils/PathTracker'; import * as NodeType from './NodeType'; +import { ExpressionEntity } from './shared/Expression'; import { ExpressionNode, NodeBase } from './shared/Node'; export default class SpreadElement extends NodeBase { @@ -7,6 +9,12 @@ export default class SpreadElement extends NodeBase { type!: NodeType.tSpreadElement; protected deoptimized = false; + deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity, recursionTracker: PathTracker) { + if (path.length > 0) { + this.argument.deoptimizeThisOnEventAtPath(event, [UnknownKey, ...path], thisParameter, recursionTracker) + } + } + protected applyDeoptimizations(): void { this.deoptimized = true; // Only properties of properties of the argument could become subject to reassignment diff --git a/src/ast/nodes/shared/ArrayPrototype.ts b/src/ast/nodes/shared/ArrayPrototype.ts new file mode 100644 index 00000000000..d01ca084612 --- /dev/null +++ b/src/ast/nodes/shared/ArrayPrototype.ts @@ -0,0 +1,144 @@ +import { UnknownInteger } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER } from '../../values'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; +import { + Method, + METHOD_RETURNS_BOOLEAN, + METHOD_RETURNS_NUMBER, + METHOD_RETURNS_STRING, + METHOD_RETURNS_UNKNOWN +} from './MethodTypes'; +import { ObjectEntity, ObjectProperty } from './ObjectEntity'; +import { OBJECT_PROTOTYPE } from './ObjectPrototype'; + +const NEW_ARRAY_PROPERTIES: ObjectProperty[] = [ + { kind: 'init', key: UnknownInteger, property: UNKNOWN_EXPRESSION }, + { kind: 'init', key: 'length', property: UNKNOWN_LITERAL_NUMBER } +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +export const METHOD_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: () => new ObjectEntity(NEW_ARRAY_PROPERTIES, ARRAY_PROTOTYPE), + returnsPrimitive: null + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_NUMBER: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; + +const METHOD_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +const METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF: [ExpressionEntity] = [ + new Method({ + callsArgs: [0], + mutatesSelfAsArray: true, + returns: 'self', + returnsPrimitive: null + }) +]; + +export const ARRAY_PROTOTYPE = new ObjectEntity( + { + // @ts-ignore + __proto__: null, + // We assume that accessors have effects as we do not track the accessed value afterwards + at: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + concat: METHOD_RETURNS_NEW_ARRAY, + copyWithin: METHOD_MUTATES_SELF_RETURNS_SELF, + entries: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + every: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + fill: METHOD_MUTATES_SELF_RETURNS_SELF, + filter: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + find: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + findIndex: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NUMBER, + forEach: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + includes: METHOD_RETURNS_BOOLEAN, + indexOf: METHOD_RETURNS_NUMBER, + join: METHOD_RETURNS_STRING, + keys: METHOD_RETURNS_UNKNOWN, + lastIndexOf: METHOD_RETURNS_NUMBER, + map: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_NEW_ARRAY, + pop: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + push: METHOD_MUTATES_SELF_RETURNS_NUMBER, + reduce: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reduceRight: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_UNKNOWN, + reverse: METHOD_MUTATES_SELF_RETURNS_SELF, + shift: METHOD_MUTATES_SELF_RETURNS_UNKNOWN, + slice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + some: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_BOOLEAN, + sort: METHOD_CALLS_ARG_MUTATES_SELF_RETURNS_SELF, + splice: METHOD_MUTATES_SELF_RETURNS_NEW_ARRAY, + unshift: METHOD_MUTATES_SELF_RETURNS_NUMBER, + values: METHOD_MUTATES_SELF_RETURNS_UNKNOWN + }, + OBJECT_PROTOTYPE +); diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index 3c8b7fcd1d3..ef21c7159e4 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -68,14 +68,11 @@ export default class ClassNode extends NodeBase implements DeoptimizableEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { - return this.getObjectEntity().getReturnExpressionWhenCalledAtPath( - path, - recursionTracker, - origin - ); + return this.getObjectEntity().getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin); } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { diff --git a/src/ast/nodes/shared/Expression.ts b/src/ast/nodes/shared/Expression.ts index 55ecbc867a6..4a851c2f523 100644 --- a/src/ast/nodes/shared/Expression.ts +++ b/src/ast/nodes/shared/Expression.ts @@ -41,6 +41,7 @@ export class ExpressionEntity implements WritableEntity { getReturnExpressionWhenCalledAtPath( _path: ObjectPath, + _callOptions: CallOptions, _recursionTracker: PathTracker, _origin: DeoptimizableEntity ): ExpressionEntity { diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index fb2f9cba34c..8976dbcb115 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -2,13 +2,14 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { DeoptimizableEntity } from '../../DeoptimizableEntity'; import { HasEffectsContext } from '../../ExecutionContext'; import { EVENT_ACCESSED, EVENT_ASSIGNED, EVENT_CALLED, NodeEvent } from '../../NodeEvents'; -import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER } from '../../utils/PathTracker'; -import PrivateIdentifier from '../PrivateIdentifier'; import { - ExpressionEntity, - LiteralValueOrUnknown, - UNKNOWN_EXPRESSION -} from './Expression'; + EMPTY_PATH, + ObjectPath, + PathTracker, + SHARED_RECURSION_TRACKER +} from '../../utils/PathTracker'; +import PrivateIdentifier from '../PrivateIdentifier'; +import { ExpressionEntity, LiteralValueOrUnknown, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode, NodeBase } from './Node'; import { PatternNode } from './Pattern'; @@ -74,11 +75,13 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.getAccessedValue().getReturnExpressionWhenCalledAtPath( path, + callOptions, recursionTracker, origin ); @@ -116,6 +119,7 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity this.accessedValue = UNKNOWN_EXPRESSION; return (this.accessedValue = this.value.getReturnExpressionWhenCalledAtPath( EMPTY_PATH, + this.accessorCallOptions, SHARED_RECURSION_TRACKER, this )); diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index 9a0dc147361..a5c20c4dac9 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -1,18 +1,18 @@ import { CallOptions, NO_ARGS } from '../../CallOptions'; import { HasEffectsContext, InclusionContext } from '../../ExecutionContext'; import { EVENT_CALLED, NodeEvent } from '../../NodeEvents'; -import { EMPTY_PATH, ObjectPath, UNKNOWN_PATH } from '../../utils/PathTracker'; -import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_STRING } from '../../values'; +import { EMPTY_PATH, ObjectPath, UNKNOWN_INTEGER_PATH } from '../../utils/PathTracker'; +import { UNKNOWN_LITERAL_BOOLEAN, UNKNOWN_LITERAL_NUMBER, UNKNOWN_LITERAL_STRING } from '../../values'; import SpreadElement from '../SpreadElement'; import { ExpressionEntity, UNKNOWN_EXPRESSION } from './Expression'; import { ExpressionNode } from './Node'; type MethodDescription = { callsArgs: number[] | null; - mutatesSelf: boolean; + mutatesSelfAsArray: boolean; } & ( | { - returns: { new (): ExpressionEntity }; + returns: 'self' | (() => ExpressionEntity); returnsPrimitive: null; } | { @@ -21,22 +21,30 @@ type MethodDescription = { } ); -class Method extends ExpressionEntity { +export class Method extends ExpressionEntity { constructor(private readonly description: MethodDescription) { super(); } deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { - if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelf) { - thisParameter.deoptimizePath(UNKNOWN_PATH); + if (event === EVENT_CALLED && path.length === 0 && this.description.mutatesSelfAsArray) { + thisParameter.deoptimizePath(UNKNOWN_INTEGER_PATH); } } - getReturnExpressionWhenCalledAtPath(path: ObjectPath): ExpressionEntity { + getReturnExpressionWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions + ): ExpressionEntity { if (path.length > 0) { return UNKNOWN_EXPRESSION; } - return this.description.returnsPrimitive || new this.description.returns(); + return ( + this.description.returnsPrimitive || + (this.description.returns === 'self' + ? callOptions.thisParam || UNKNOWN_EXPRESSION + : this.description.returns()) + ); } hasEffectsWhenAccessedAtPath(path: ObjectPath): boolean { @@ -52,7 +60,11 @@ class Method extends ExpressionEntity { callOptions: CallOptions, context: HasEffectsContext ): boolean { - if (path.length > 0) { + if ( + path.length > 0 || + (this.description.mutatesSelfAsArray && + callOptions.thisParam?.hasEffectsWhenAssignedAtPath(UNKNOWN_INTEGER_PATH, context)) + ) { return true; } if (!this.description.callsArgs) { @@ -84,23 +96,38 @@ class Method extends ExpressionEntity { } } -export const METHOD_RETURNS_BOOLEAN = new Method({ - callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN -}); +export const METHOD_RETURNS_BOOLEAN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + }) +]; + +export const METHOD_RETURNS_STRING = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_STRING + }) +]; -export const METHOD_RETURNS_STRING = new Method({ - callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING -}); +export const METHOD_RETURNS_NUMBER = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_LITERAL_NUMBER + }) +]; -export const METHOD_RETURNS_UNKNOWN = new Method({ - callsArgs: null, - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_EXPRESSION -}); +export const METHOD_RETURNS_UNKNOWN = [ + new Method({ + callsArgs: null, + mutatesSelfAsArray: false, + returns: null, + returnsPrimitive: UNKNOWN_EXPRESSION + }) +]; diff --git a/src/ast/nodes/shared/MultiExpression.ts b/src/ast/nodes/shared/MultiExpression.ts index f5562a30c30..b7d7426b5db 100644 --- a/src/ast/nodes/shared/MultiExpression.ts +++ b/src/ast/nodes/shared/MultiExpression.ts @@ -20,12 +20,13 @@ export class MultiExpression extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return new MultiExpression( this.expressions.map(expression => - expression.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin) + expression.getReturnExpressionWhenCalledAtPath(path, callOptions, recursionTracker, origin) ) ); } diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 90f898685ab..3ddea201473 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -6,7 +6,9 @@ import { ObjectPath, ObjectPathKey, PathTracker, + UnknownInteger, UnknownKey, + UNKNOWN_INTEGER_PATH, UNKNOWN_PATH } from '../../utils/PathTracker'; import { @@ -23,6 +25,7 @@ export interface ObjectProperty { } type PropertyMap = Record; +const INTEGER_REG_EXP = /^\d+$/; export class ObjectEntity extends ExpressionEntity { private readonly allProperties: ExpressionEntity[] = []; @@ -32,11 +35,13 @@ export class ObjectEntity extends ExpressionEntity { DeoptimizableEntity[] > = Object.create(null); private readonly gettersByKey: PropertyMap = Object.create(null); + private hasUnknownDeoptimizedInteger = false; private hasUnknownDeoptimizedProperty = false; private readonly propertiesAndGettersByKey: PropertyMap = Object.create(null); private readonly propertiesAndSettersByKey: PropertyMap = Object.create(null); private readonly settersByKey: PropertyMap = Object.create(null); private readonly thisParametersToBeDeoptimized = new Set(); + private readonly unknownIntegerProps: ExpressionEntity[] = []; private readonly unmatchableGetters: ExpressionEntity[] = []; private readonly unmatchablePropertiesAndGetters: ExpressionEntity[] = []; private readonly unmatchableSetters: ExpressionEntity[] = []; @@ -72,14 +77,22 @@ export class ObjectEntity extends ExpressionEntity { } // While the prototype itself cannot be mutated, each property can this.prototypeExpression?.deoptimizePath([UnknownKey, UnknownKey]); - for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { - for (const expression of expressionsToBeDeoptimized) { - expression.deoptimizeCache(); - } + this.deoptimizeCachedEntities(); + } + + deoptimizeIntegerProperties(): void { + if (this.hasUnknownDeoptimizedProperty || this.hasUnknownDeoptimizedInteger) { + return; } - for (const expression of this.thisParametersToBeDeoptimized) { - expression.deoptimizePath(UNKNOWN_PATH); + this.hasUnknownDeoptimizedInteger = true; + for (const key of Object.keys(this.propertiesAndGettersByKey)) { + if (INTEGER_REG_EXP.test(key)) { + for (const property of this.propertiesAndGettersByKey[key]) { + property.deoptimizePath(UNKNOWN_PATH); + } + } } + this.deoptimizeCachedIntegerEntities(); } deoptimizePath(path: ObjectPath) { @@ -87,8 +100,10 @@ export class ObjectEntity extends ExpressionEntity { const key = path[0]; if (path.length === 1) { if (typeof key !== 'string') { - this.deoptimizeAllProperties(); - return; + if (key === UnknownInteger) { + return this.deoptimizeIntegerProperties(); + } + return this.deoptimizeAllProperties(); } if (!this.deoptimizedPaths[key]) { this.deoptimizedPaths[key] = true; @@ -137,7 +152,6 @@ export class ObjectEntity extends ExpressionEntity { return; } - this.thisParametersToBeDeoptimized.add(thisParameter); const [propertiesForExactMatchByKey, relevantPropertiesByKey, relevantUnmatchableProperties] = event === EVENT_CALLED || path.length > 1 ? [ @@ -157,11 +171,17 @@ export class ObjectEntity extends ExpressionEntity { property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } } + this.thisParametersToBeDeoptimized.add(thisParameter); return; } for (const property of relevantUnmatchableProperties) { property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } + if (INTEGER_REG_EXP.test(key)) { + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } + } } else { for (const properties of Object.values(relevantPropertiesByKey).concat([ relevantUnmatchableProperties @@ -170,7 +190,11 @@ export class ObjectEntity extends ExpressionEntity { property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); } } + for (const property of this.unknownIntegerProps) { + property.deoptimizeThisOnEventAtPath(event, subPath, thisParameter, recursionTracker); + } } + this.thisParametersToBeDeoptimized.add(thisParameter); this.prototypeExpression?.deoptimizeThisOnEventAtPath( event, path, @@ -203,6 +227,7 @@ export class ObjectEntity extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -214,6 +239,7 @@ export class ObjectEntity extends ExpressionEntity { if (expressionAtPath) { return expressionAtPath.getReturnExpressionWhenCalledAtPath( path.slice(1), + callOptions, recursionTracker, origin ); @@ -221,6 +247,7 @@ export class ObjectEntity extends ExpressionEntity { if (this.prototypeExpression) { return this.prototypeExpression.getReturnExpressionWhenCalledAtPath( path, + callOptions, recursionTracker, origin ); @@ -246,11 +273,17 @@ export class ObjectEntity extends ExpressionEntity { if (this.hasUnknownDeoptimizedProperty) return true; if (typeof key === 'string') { - for (const property of this.gettersByKey[key] || this.unmatchableGetters) { - if (property.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + if (this.propertiesAndGettersByKey[key]) { + const getters = this.gettersByKey[key]; + if (getters) { + for (const getter of getters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; + } + } + return false; } - if (this.prototypeExpression && !this.propertiesAndGettersByKey[key]) { - return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); + for (const getter of this.unmatchableGetters) { + if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; } } else { for (const getters of Object.values(this.gettersByKey).concat([this.unmatchableGetters])) { @@ -258,9 +291,9 @@ export class ObjectEntity extends ExpressionEntity { if (getter.hasEffectsWhenAccessedAtPath(subPath, context)) return true; } } - if (this.prototypeExpression) { - return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); - } + } + if (this.prototypeExpression) { + return this.prototypeExpression.hasEffectsWhenAccessedAtPath(path, context); } return false; } @@ -281,13 +314,28 @@ export class ObjectEntity extends ExpressionEntity { return true; } - if (typeof key !== 'string') return true; - - const properties = this.settersByKey[key] || this.unmatchableSetters; - for (const property of properties) { - if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + if (this.hasUnknownDeoptimizedProperty) return true; + if (typeof key === 'string') { + if (this.propertiesAndSettersByKey[key]) { + const setters = this.settersByKey[key]; + if (setters) { + for (const setter of setters) { + if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } + return false; + } + for (const property of this.unmatchableSetters) { + if (property.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } else { + for (const setters of Object.values(this.settersByKey).concat([this.unmatchableSetters])) { + for (const setter of setters) { + if (setter.hasEffectsWhenAssignedAtPath(subPath, context)) return true; + } + } } - if (this.prototypeExpression && !this.settersByKey[key]) { + if (this.prototypeExpression) { return this.prototypeExpression.hasEffectsWhenAssignedAtPath(path, context); } return false; @@ -316,6 +364,7 @@ export class ObjectEntity extends ExpressionEntity { propertiesAndSettersByKey, settersByKey, gettersByKey, + unknownIntegerProps, unmatchablePropertiesAndGetters, unmatchableGetters, unmatchableSetters @@ -325,6 +374,10 @@ export class ObjectEntity extends ExpressionEntity { const { key, kind, property } = properties[index]; allProperties.push(property); if (typeof key !== 'string') { + if (key === UnknownInteger) { + unknownIntegerProps.push(property); + continue; + } if (kind === 'set') unmatchableSetters.push(property); if (kind === 'get') unmatchableGetters.push(property); if (kind !== 'get') unmatchablePropertiesAndSetters.push(property); @@ -346,12 +399,44 @@ export class ObjectEntity extends ExpressionEntity { } if (!propertiesAndGettersByKey[key]) { propertiesAndGettersByKey[key] = [property, ...unmatchablePropertiesAndGetters]; + if (INTEGER_REG_EXP.test(key)) { + for (const integerProperty of unknownIntegerProps) { + propertiesAndGettersByKey[key].push(integerProperty); + } + } } } } } } + private deoptimizeCachedEntities() { + for (const expressionsToBeDeoptimized of Object.values(this.expressionsToBeDeoptimizedByKey)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_PATH); + } + } + + // TODO Lukas check everywhere if we can replace Object.keys with Object.entries/values + private deoptimizeCachedIntegerEntities() { + for (const [key, expressionsToBeDeoptimized] of Object.entries( + this.expressionsToBeDeoptimizedByKey + )) { + if (INTEGER_REG_EXP.test(key)) { + for (const expression of expressionsToBeDeoptimized) { + expression.deoptimizeCache(); + } + } + } + for (const expression of this.thisParametersToBeDeoptimized) { + expression.deoptimizePath(UNKNOWN_INTEGER_PATH); + } + } + private getMemberExpression(key: ObjectPathKey): ExpressionEntity | null { if ( this.hasUnknownDeoptimizedProperty || @@ -364,7 +449,11 @@ export class ObjectEntity extends ExpressionEntity { if (properties?.length === 1) { return properties[0]; } - if (properties || this.unmatchablePropertiesAndGetters.length > 0) { + if ( + properties || + this.unmatchablePropertiesAndGetters.length > 0 || + (this.unknownIntegerProps.length && INTEGER_REG_EXP.test(key)) + ) { return UNKNOWN_EXPRESSION; } return null; @@ -374,7 +463,7 @@ export class ObjectEntity extends ExpressionEntity { key: ObjectPathKey, origin: DeoptimizableEntity ): ExpressionEntity | null { - if (key === UnknownKey) { + if (typeof key !== 'string') { return UNKNOWN_EXPRESSION; } const expression = this.getMemberExpression(key); diff --git a/src/ast/nodes/shared/ObjectMember.ts b/src/ast/nodes/shared/ObjectMember.ts index b91c168a2ca..d72db17c612 100644 --- a/src/ast/nodes/shared/ObjectMember.ts +++ b/src/ast/nodes/shared/ObjectMember.ts @@ -33,11 +33,13 @@ export class ObjectMember extends ExpressionEntity { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { return this.object.getReturnExpressionWhenCalledAtPath( [this.key, ...path], + callOptions, recursionTracker, origin ); diff --git a/src/ast/nodes/shared/ObjectPrototype.ts b/src/ast/nodes/shared/ObjectPrototype.ts index d3a12b0468c..88e20e2d7a0 100644 --- a/src/ast/nodes/shared/ObjectPrototype.ts +++ b/src/ast/nodes/shared/ObjectPrototype.ts @@ -7,12 +7,20 @@ import { ObjectEntity } from './ObjectEntity'; export const OBJECT_PROTOTYPE = new ObjectEntity( { - hasOwnProperty: [METHOD_RETURNS_BOOLEAN], - isPrototypeOf: [METHOD_RETURNS_BOOLEAN], - propertyIsEnumerable: [METHOD_RETURNS_BOOLEAN], - toLocaleString: [METHOD_RETURNS_STRING], - toString: [METHOD_RETURNS_STRING], - valueOf: [METHOD_RETURNS_UNKNOWN] + // @ts-ignore + __proto__: null, + // @ts-ignore + hasOwnProperty: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + isPrototypeOf: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + propertyIsEnumerable: METHOD_RETURNS_BOOLEAN, + // @ts-ignore + toLocaleString: METHOD_RETURNS_STRING, + // @ts-ignore + toString: METHOD_RETURNS_STRING, + // @ts-ignore + valueOf: METHOD_RETURNS_UNKNOWN }, null ); diff --git a/src/ast/utils/PathTracker.ts b/src/ast/utils/PathTracker.ts index 0215fcfc96c..0c2d8306e27 100644 --- a/src/ast/utils/PathTracker.ts +++ b/src/ast/utils/PathTracker.ts @@ -2,16 +2,19 @@ import { getOrCreate } from '../../utils/getOrCreate'; import { Entity } from '../Entity'; export const UnknownKey = Symbol('Unknown Key'); -export type ObjectPathKey = string | typeof UnknownKey; +export const UnknownInteger = Symbol('Unknown Integer'); +export type ObjectPathKey = string | typeof UnknownKey | typeof UnknownInteger; export type ObjectPath = ObjectPathKey[]; export const EMPTY_PATH: ObjectPath = []; export const UNKNOWN_PATH: ObjectPath = [UnknownKey]; +export const UNKNOWN_INTEGER_PATH: ObjectPath = [UnknownInteger]; const EntitiesKey = Symbol('Entities'); interface EntityPaths { [EntitiesKey]: Set; [UnknownKey]?: EntityPaths; + [UnknownInteger]?: EntityPaths; [pathSegment: string]: EntityPaths; } @@ -57,6 +60,7 @@ export const SHARED_RECURSION_TRACKER = new PathTracker(); interface DiscriminatedEntityPaths { [EntitiesKey]: Map>; [UnknownKey]?: DiscriminatedEntityPaths; + [UnknownInteger]?: DiscriminatedEntityPaths; [pathSegment: string]: DiscriminatedEntityPaths; } diff --git a/src/ast/values.ts b/src/ast/values.ts index ee65bc908d9..f05f1ab700b 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -40,79 +40,6 @@ const returnsUnknown: RawMemberDescription = { returnsPrimitive: UNKNOWN_EXPRESSION } }; -const mutatesSelfReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: null, mutatesSelf: true } -}; -const callsArgReturnsUnknown: RawMemberDescription = { - value: { returns: null, returnsPrimitive: UNKNOWN_EXPRESSION, callsArgs: [0], mutatesSelf: false } -}; - -export class UnknownArrayExpression extends ExpressionEntity { - included = false; - - getReturnExpressionWhenCalledAtPath(path: ObjectPath) { - if (path.length === 1) { - return getMemberReturnExpressionWhenCalled(arrayMembers, path[0]); - } - return UNKNOWN_EXPRESSION; - } - - hasEffectsWhenAccessedAtPath(path: ObjectPath) { - return path.length > 1; - } - - hasEffectsWhenAssignedAtPath(path: ObjectPath) { - return path.length > 1; - } - - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - if (path.length === 1) { - return hasMemberEffectWhenCalled(arrayMembers, path[0], this.included, callOptions, context); - } - return true; - } - - include() { - this.included = true; - } -} - -const returnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const mutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; -const callsArgMutatesSelfReturnsArray: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: true, - returns: UnknownArrayExpression, - returnsPrimitive: null - } -}; export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoolean extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { @@ -143,16 +70,8 @@ const returnsBoolean: RawMemberDescription = { returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN } }; -const callsArgReturnsBoolean: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN - } -}; -const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber extends ExpressionEntity { +export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { if (path.length === 1) { return getMemberReturnExpressionWhenCalled(literalNumberMembers, path[0]); @@ -181,22 +100,6 @@ const returnsNumber: RawMemberDescription = { returnsPrimitive: UNKNOWN_LITERAL_NUMBER } }; -const mutatesSelfReturnsNumber: RawMemberDescription = { - value: { - callsArgs: null, - mutatesSelf: true, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; -const callsArgReturnsNumber: RawMemberDescription = { - value: { - callsArgs: [0], - mutatesSelf: false, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER - } -}; export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString extends ExpressionEntity { getReturnExpressionWhenCalledAtPath(path: ObjectPath) { @@ -240,37 +143,6 @@ const objectMembers: MemberDescriptions = assembleMemberDescriptions({ valueOf: returnsUnknown }); -// TODO Lukas first make arrays proper objects, then get rid of this -export const arrayMembers: MemberDescriptions = assembleMemberDescriptions( - { - concat: returnsArray, - copyWithin: mutatesSelfReturnsArray, - every: callsArgReturnsBoolean, - fill: mutatesSelfReturnsArray, - filter: callsArgReturnsArray, - find: callsArgReturnsUnknown, - findIndex: callsArgReturnsNumber, - forEach: callsArgReturnsUnknown, - includes: returnsBoolean, - indexOf: returnsNumber, - join: returnsString, - lastIndexOf: returnsNumber, - map: callsArgReturnsArray, - pop: mutatesSelfReturnsUnknown, - push: mutatesSelfReturnsNumber, - reduce: callsArgReturnsUnknown, - reduceRight: callsArgReturnsUnknown, - reverse: mutatesSelfReturnsArray, - shift: mutatesSelfReturnsUnknown, - slice: returnsArray, - some: callsArgReturnsBoolean, - sort: callsArgMutatesSelfReturnsArray, - splice: mutatesSelfReturnsArray, - unshift: mutatesSelfReturnsNumber - }, - objectMembers -); - const literalBooleanMembers: MemberDescriptions = assembleMemberDescriptions( { valueOf: returnsBoolean @@ -315,7 +187,7 @@ const literalStringMembers: MemberDescriptions = assembleMemberDescriptions( }, search: returnsNumber, slice: returnsString, - split: returnsArray, + split: returnsUnknown, startsWith: returnsBoolean, substr: returnsString, substring: returnsString, diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 8981269ff73..92573a8a3c7 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -129,6 +129,7 @@ export default class LocalVariable extends Variable { getReturnExpressionWhenCalledAtPath( path: ObjectPath, + callOptions: CallOptions, recursionTracker: PathTracker, origin: DeoptimizableEntity ): ExpressionEntity { @@ -140,7 +141,12 @@ export default class LocalVariable extends Variable { this.init, () => { this.expressionsToBeDeoptimized.push(origin); - return this.init!.getReturnExpressionWhenCalledAtPath(path, recursionTracker, origin); + return this.init!.getReturnExpressionWhenCalledAtPath( + path, + callOptions, + recursionTracker, + origin + ); }, UNKNOWN_EXPRESSION ); diff --git a/test/form/samples/array-element-tracking/array-elements/_config.js b/test/form/samples/array-element-tracking/array-elements/_config.js new file mode 100644 index 00000000000..46d8f9b2d3d --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays' +}; diff --git a/test/form/samples/array-element-tracking/array-elements/_expected.js b/test/form/samples/array-element-tracking/array-elements/_expected.js new file mode 100644 index 00000000000..350400b9918 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/_expected.js @@ -0,0 +1,4 @@ +console.log('retained'); +console.log('retained'); +console.log('retained'); +console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-elements/main.js b/test/form/samples/array-element-tracking/array-elements/main.js new file mode 100644 index 00000000000..31792cf92ec --- /dev/null +++ b/test/form/samples/array-element-tracking/array-elements/main.js @@ -0,0 +1,10 @@ +const array = [true, false, 3]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2] === 3) console.log('retained'); +else console.log('removed'); +if (array[3]) console.log('removed'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-mutation/_config.js b/test/form/samples/array-element-tracking/array-mutation/_config.js new file mode 100644 index 00000000000..58a8183eebf --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks mutations of array elements' +}; diff --git a/test/form/samples/array-element-tracking/array-mutation/_expected.js b/test/form/samples/array-element-tracking/array-mutation/_expected.js new file mode 100644 index 00000000000..b4c4acc5b9a --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/_expected.js @@ -0,0 +1,10 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +console.log('retained'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-mutation/main.js b/test/form/samples/array-element-tracking/array-mutation/main.js new file mode 100644 index 00000000000..37327b20616 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-mutation/main.js @@ -0,0 +1,11 @@ +const array = [true, true]; + +array[1] = false; +array[2] = true; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('unimportant'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); diff --git a/test/form/samples/array-element-tracking/array-spread/_config.js b/test/form/samples/array-element-tracking/array-spread/_config.js new file mode 100644 index 00000000000..57b80f7746f --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks elements of arrays with spread elements' +}; diff --git a/test/form/samples/array-element-tracking/array-spread/_expected.js b/test/form/samples/array-element-tracking/array-spread/_expected.js new file mode 100644 index 00000000000..2d0ec880684 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/_expected.js @@ -0,0 +1,9 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +console.log('retained'); +console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/array-spread/main.js b/test/form/samples/array-element-tracking/array-spread/main.js new file mode 100644 index 00000000000..cf36dae8591 --- /dev/null +++ b/test/form/samples/array-element-tracking/array-spread/main.js @@ -0,0 +1,11 @@ +const spread = [true, false]; +const array = [true, false, ...spread]; + +if (array[0]) console.log('retained'); +else console.log('removed'); +if (array[1]) console.log('removed'); +else console.log('retained'); +if (array[2]) console.log('retained'); +else console.log('unimportant'); +if (array[3]) console.log('unimportant'); +else console.log('retained'); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js new file mode 100644 index 00000000000..b1ce2636a6d --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'deoptimizes array spread elements' +}; diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/_expected.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js new file mode 100644 index 00000000000..a2633a81cc0 --- /dev/null +++ b/test/form/samples/array-element-tracking/spread-element-deoptimization/main.js @@ -0,0 +1,3 @@ +const spread = [{ effect() {} }]; +[...spread][0].effect = () => console.log('effect'); +spread[0].effect(); diff --git a/test/form/samples/builtin-prototypes/array-expression/_expected.js b/test/form/samples/builtin-prototypes/array-expression/_expected.js index 6d040eeda50..c0eda2afc00 100644 --- a/test/form/samples/builtin-prototypes/array-expression/_expected.js +++ b/test/form/samples/builtin-prototypes/array-expression/_expected.js @@ -1,31 +1,76 @@ -[ 1 ].map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => console.log( 1 ) ).map( x => x ); -[ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -[ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +[1].map(x => console.log(1)); +[1].map(x => x).map(x => console.log(1)); +[1].map(x => console.log(1)).map(x => x); +[1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +[1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); -[ 1 ].every( () => console.log( 1 ) || true ); -[ 1 ].filter( () => console.log( 1 ) || true ); -[ 1 ].find( () => console.log( 1 ) || true ); -[ 1 ].findIndex( () => console.log( 1 ) || true ); -[ 1 ].forEach( () => console.log( 1 ) || true ); -[ 1 ].map( () => console.log( 1 ) || 1 ); -[ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -[ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -[ 1 ].some( () => console.log( 1 ) || true ); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); +[1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); +[1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); +[1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); +[1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); +[1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); +[1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); +[1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); +[1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); +[1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -const exported = [ 1 ]; -exported.copyWithin( 0 ); -exported.fill( 0 ); +const exported = [1]; +exported.copyWithin(0); +exported.fill(0); exported.pop(); -exported.push( 0 ); +exported.push(0); exported.reverse(); exported.shift(); -[ 1 ].sort( () => console.log( 1 ) || 0 ); +[1].sort(() => console.log(1) || 0); exported.sort(); -exported.splice( 0 ); -exported.unshift( 0 ); +exported.splice(0); +exported.unshift(0); export { exported }; diff --git a/test/form/samples/builtin-prototypes/array-expression/main.js b/test/form/samples/builtin-prototypes/array-expression/main.js index a04bdfa57e3..0f7b94cf367 100644 --- a/test/form/samples/builtin-prototypes/array-expression/main.js +++ b/test/form/samples/builtin-prototypes/array-expression/main.js @@ -1,73 +1,140 @@ const array = []; -const join1 = array.join( ',' ); -const join2 = [].join( ',' ); -const join3 = [].join( ',' ).trim(); +const join1 = array.join(','); +const join2 = [].join(','); +const join3 = [].join(',').trim(); const length = [].length; -const map1 = [ 1 ].map( x => x ); -const map2 = [ 1 ].map( x => console.log( 1 ) ); -const map3 = [ 1 ].map( x => x ).map( x => x ); -const map4 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ); -const map5 = [ 1 ].map( x => console.log( 1 ) ).map( x => x ); -const map6 = [ 1 ].map( x => x ).map( x => x ).map( x => x ); -const map7 = [ 1 ].map( x => x ).map( x => x ).map( x => console.log( 1 ) ); -const map8 = [ 1 ].map( x => x ).map( x => console.log( 1 ) ).map( x => x ); +const map1 = [1].map(x => x); +const map2 = [1].map(x => console.log(1)); +const map3 = [1].map(x => x).map(x => x); +const map4 = [1].map(x => x).map(x => console.log(1)); +const map5 = [1].map(x => console.log(1)).map(x => x); +const map6 = [1] + .map(x => x) + .map(x => x) + .map(x => x); +const map7 = [1] + .map(x => x) + .map(x => x) + .map(x => console.log(1)); +const map8 = [1] + .map(x => x) + .map(x => console.log(1)) + .map(x => x); [](); // accessor methods -const _includes = [].includes( 1 ).valueOf(); -const _indexOf = [].indexOf( 1 ).toPrecision( 1 ); -const _join = [].join( ',' ).trim(); -const _lastIndexOf = [].lastIndexOf( 1 ).toPrecision( 1 ); -const _slice = [].slice( 1 ).concat( [] ); +const removedTestArray = [{ noEffect() {} }]; +removedTestArray[0].noEffect(); + +const _at = [].at(1); +const _atArray = [{ effect() {} }]; +_atArray.at(0).effect = () => console.log(1); +_atArray[0].effect(); + +const _entries = [].entries(); +const _entriesArray = [{ effect() {} }]; +[..._entriesArray.entries()][0][1].effect = () => console.log(1); +_entriesArray[0].effect(); + +const _includes = [].includes(1).valueOf(); +const _indexOf = [].indexOf(1).toPrecision(1); +const _join = [].join(',').trim(); +const _keys = [].keys(); +const _lastIndexOf = [].lastIndexOf(1).toPrecision(1); + +const _slice = [].slice(1).concat([]); +const _sliceArray = [{ effect() {} }]; +_sliceArray.slice()[0].effect = () => console.log(1); +_sliceArray[0].effect(); + +const _values = [].values(); +const _valuesArray = [{ effect() {} }]; +[..._valuesArray.values()][0].effect = () => console.log(1); +_valuesArray[0].effect(); // iteration methods -const _every = [ 1 ].every( () => true ).valueOf(); -const _everyEffect = [ 1 ].every( () => console.log( 1 ) || true ); -const _filter = [ 1 ].filter( () => true ).join( ',' ); -const _filterEffect = [ 1 ].filter( () => console.log( 1 ) || true ); -const _find = [ 1 ].find( () => true ); -const _findEffect = [ 1 ].find( () => console.log( 1 ) || true ); -const _findIndex = [ 1 ].findIndex( () => true ).toPrecision( 1 ); -const _findIndexEffect = [ 1 ].findIndex( () => console.log( 1 ) || true ); -const _forEach = [ 1 ].forEach( () => {} ); -const _forEachEffect = [ 1 ].forEach( () => console.log( 1 ) || true ); -const _map = [ 1 ].map( () => 1 ).join( ',' ); -const _mapEffect = [ 1 ].map( () => console.log( 1 ) || 1 ); -const _reduce = [ 1 ].reduce( () => 1, 1 ); -const _reduceEffect = [ 1 ].reduce( () => console.log( 1 ) || 1, 1 ); -const _reduceRight = [ 1 ].reduceRight( () => 1, 1 ); -const _reduceRightEffect = [ 1 ].reduceRight( () => console.log( 1 ) || 1, 1 ); -const _some = [ 1 ].some( () => true ).valueOf(); -const _someEffect = [ 1 ].some( () => console.log( 1 ) || true ); +const _every = [1].every(() => true).valueOf(); +const _everyEffect = [1].every(() => console.log(1) || true); +const _everyArray = [{ effect() {} }]; +_everyArray.every(element => (element.effect = () => console.log(1))); +_everyArray[0].effect(); + +const _filter = [1].filter(() => true).join(','); +const _filterEffect = [1].filter(() => console.log(1) || true); +const _filterArray = [{ effect() {} }]; +_filterArray.filter(element => (element.effect = () => console.log(1))); +_filterArray[0].effect(); + +const _find = [1].find(() => true); +const _findEffect = [1].find(() => console.log(1) || true); +const _findArray = [{ effect() {} }]; +_findArray.find(element => (element.effect = () => console.log(1))); +_findArray[0].effect(); + +const _findIndex = [1].findIndex(() => true).toPrecision(1); +const _findIndexEffect = [1].findIndex(() => console.log(1) || true); +const _findIndexArray = [{ effect() {} }]; +_findIndexArray.findIndex(element => (element.effect = () => console.log(1))); +_findIndexArray[0].effect(); + +const _forEach = [1].forEach(() => {}); +const _forEachEffect = [1].forEach(() => console.log(1) || true); +const _forEachArray = [{ effect() {} }]; +_forEachArray.forEach(element => (element.effect = () => console.log(1))); +_forEachArray[0].effect(); + +const _map = [1].map(() => 1).join(','); +const _mapEffect = [1].map(() => console.log(1) || 1); +const _mapArray = [{ effect() {} }]; +_mapArray.map(element => (element.effect = () => console.log(1))); +_mapArray[0].effect(); + +const _reduce = [1].reduce(() => 1, 1); +const _reduceEffect = [1].reduce(() => console.log(1) || 1, 1); +const _reduceArray = [{ effect() {} }]; +_reduceArray.reduce((_, element) => (element.effect = () => console.log(1)), 1); +_reduceArray[0].effect(); + +const _reduceRight = [1].reduceRight(() => 1, 1); +const _reduceRightEffect = [1].reduceRight(() => console.log(1) || 1, 1); +const _reduceRightArray = [{ effect() {} }]; +_reduceRightArray.reduceRight((_, element) => (element.effect = () => console.log(1)), 1); +_reduceRightArray[0].effect(); + +const _some = [1].some(() => true).valueOf(); +const _someEffect = [1].some(() => console.log(1) || true); +const _someArray = [{ effect() {} }]; +_someArray.some(element => (element.effect = () => console.log(1))); +_someArray[0].effect(); // mutator methods -export const exported = [ 1 ]; +export const exported = [1]; -const _copyWithin = [ 1 ].copyWithin( 0 ).join( ',' ); -exported.copyWithin( 0 ); -const _fill = [ 1 ].fill( 0 ).join( ',' ); -exported.fill( 0 ); -const _pop = [ 1 ].pop(); +const _copyWithin = [1].copyWithin(0).join(','); +exported.copyWithin(0); +const _fill = [1].fill(0).join(','); +exported.fill(0); +const _pop = [1].pop(); exported.pop(); -const _push = [ 1 ].push( 0 ).toPrecision( 1 ); -exported.push( 0 ); -const _reverse = [ 1 ].reverse().join( ',' ); +const _push = [1].push(0).toPrecision(1); +exported.push(0); +const _reverse = [1].reverse().join(','); exported.reverse(); -const _shift = [ 1 ].shift(); +const _shift = [1].shift(); exported.shift(); -const _sort = [ 1 ].sort( () => 0 ).join( ',' ); -const _sortEffect = [ 1 ].sort( () => console.log( 1 ) || 0 ); +const _sort = [1].sort(() => 0).join(','); +const _sortEffect = [1].sort(() => console.log(1) || 0); exported.sort(); -const _splice = [ 1 ].splice( 0 ).join( ',' ); -exported.splice( 0 ); -const _unshift = [ 1 ].unshift( 0 ).toPrecision( 1 ); -exported.unshift( 0 ); +const _splice = [1].splice(0).join(','); +exported.splice(0); +const _unshift = [1].unshift(0).toPrecision(1); +exported.unshift(0); // inherited -const _hasOwnProperty = [ 1 ].hasOwnProperty( 'toString' ).valueOf(); -const _isPrototypeOf = [ 1 ].isPrototypeOf( [] ).valueOf(); -const _propertyIsEnumerable = [ 1 ].propertyIsEnumerable( 'toString' ).valueOf(); -const _toLocaleString = [ 1 ].toLocaleString().trim(); -const _toString = [ 1 ].toString().trim(); -const _valueOf = [ 1 ].valueOf(); +const _hasOwnProperty = [1].hasOwnProperty('toString').valueOf(); +const _isPrototypeOf = [1].isPrototypeOf([]).valueOf(); +const _propertyIsEnumerable = [1].propertyIsEnumerable('toString').valueOf(); +const _toLocaleString = [1].toLocaleString().trim(); +const _toString = [1].toString().trim(); +const _valueOf = [1].valueOf(); diff --git a/test/form/samples/builtin-prototypes/literal/main.js b/test/form/samples/builtin-prototypes/literal/main.js index edba0b1ee4f..eaadeb2ba8c 100644 --- a/test/form/samples/builtin-prototypes/literal/main.js +++ b/test/form/samples/builtin-prototypes/literal/main.js @@ -56,7 +56,7 @@ const _replace = 'ab'.replace( 'a', () => 'b' ).trim(); const _replaceEffect = 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); const _search = 'ab'.search( /a/ ).toExponential( 2 ); const _slice = 'ab'.slice( 0, 1 ).trim(); -const _split = 'ab'.split( 'a' ).join(); +const _split = 'ab'.split( 'a' ); const _startsWith = 'ab'.startsWith( 'a' ).valueOf(); const _substr = 'ab'.substr( 0, 1 ).trim(); const _substring = 'ab'.substring( 0, 1 ).trim(); diff --git a/test/function/samples/array-getter/_config.js b/test/function/samples/array-getter/_config.js new file mode 100644 index 00000000000..e0437548252 --- /dev/null +++ b/test/function/samples/array-getter/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles getters defined on arrays' +}; diff --git a/test/function/samples/array-getter/main.js b/test/function/samples/array-getter/main.js new file mode 100644 index 00000000000..9ac16a7b6e6 --- /dev/null +++ b/test/function/samples/array-getter/main.js @@ -0,0 +1,14 @@ +let flag = false; +const array = []; + +Object.defineProperty(array, 'prop', { + get() { + flag = true; + } +}); + +array.prop; + +if (!flag) { + throw new Error('Mutation not detected'); +} From f4766a6262b85eff404a888a929b52319d1da404 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 15 May 2021 07:24:46 +0200 Subject: [PATCH 40/50] Simplify namespace handling --- src/ast/values.ts | 1 - src/ast/variables/NamespaceVariable.ts | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/src/ast/values.ts b/src/ast/values.ts index f05f1ab700b..30866c41324 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -235,7 +235,6 @@ export function hasMemberEffectWhenCalled( EMPTY_PATH, { args: NO_ARGS, - // TODO Lukas think about this thisParam: null, withNew: false }, diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index 0f94f09910f..c81cff49892 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -3,7 +3,6 @@ import { RenderOptions } from '../../utils/renderHelpers'; import { RESERVED_NAMES } from '../../utils/reservedNames'; import { getSystemExportStatement } from '../../utils/systemJsRendering'; import Identifier from '../nodes/Identifier'; -import { ObjectPath, UNKNOWN_PATH } from '../utils/PathTracker'; import Variable from './Variable'; export default class NamespaceVariable extends Variable { @@ -29,22 +28,6 @@ export default class NamespaceVariable extends Variable { this.name = identifier.name; } - deoptimizePath(path: ObjectPath) { - const memberVariables = this.getMemberVariables(); - const memberPath = path.length <= 1 ? UNKNOWN_PATH : path.slice(1); - const key = path[0]; - if (typeof key === 'string') { - memberVariables[key]?.deoptimizePath(memberPath); - } else { - for (const key of Object.keys(memberVariables)) { - memberVariables[key].deoptimizePath(memberPath); - } - } - } - - // TODO Lukas can this be triggered for nested paths? - deoptimizeThisOnEventAtPath() {} - getMemberVariables(): { [name: string]: Variable } { if (this.memberVariables) { return this.memberVariables; From 92b1e1d4d464bd4e48a47f2d24cb3aa9f1d79e0e Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 15 May 2021 08:08:44 +0200 Subject: [PATCH 41/50] Use Object.values and Object.entries instead of Object.keys where useful --- cli/run/build.ts | 4 +--- cli/run/watch-cli.ts | 4 +--- src/Bundle.ts | 22 +++++++++------------- src/Chunk.ts | 4 ++-- src/Graph.ts | 20 ++++++++++---------- src/Module.ts | 3 +-- src/ast/keys.ts | 2 +- src/ast/nodes/ArrowFunctionExpression.ts | 1 - src/ast/nodes/AssignmentExpression.ts | 1 - src/ast/nodes/CallExpression.ts | 1 - src/ast/nodes/NewExpression.ts | 1 - src/ast/nodes/TaggedTemplateExpression.ts | 1 - src/ast/nodes/shared/ClassNode.ts | 4 ---- src/ast/nodes/shared/FunctionNode.ts | 3 --- src/ast/nodes/shared/MethodBase.ts | 1 - src/ast/nodes/shared/MethodTypes.ts | 1 - src/ast/nodes/shared/Node.ts | 3 +-- src/ast/nodes/shared/ObjectEntity.ts | 5 ++--- src/ast/variables/NamespaceVariable.ts | 4 +--- src/rollup/rollup.ts | 13 ++++--------- src/utils/FileEmitter.ts | 10 ++++++---- src/utils/chunkAssignment.ts | 4 ++-- src/utils/options/normalizeInputOptions.ts | 4 ++-- src/utils/timers.ts | 6 +++--- 24 files changed, 46 insertions(+), 76 deletions(-) diff --git a/cli/run/build.ts b/cli/run/build.ts index 38c55e2e05f..c5fab831470 100644 --- a/cli/run/build.ts +++ b/cli/run/build.ts @@ -24,9 +24,7 @@ export default async function build( } else if (inputOptions.input instanceof Array) { inputFiles = inputOptions.input.join(', '); } else if (typeof inputOptions.input === 'object' && inputOptions.input !== null) { - inputFiles = Object.keys(inputOptions.input) - .map(name => (inputOptions.input as Record)[name]) - .join(', '); + inputFiles = Object.values(inputOptions.input).join(', '); } stderr(cyan(`\n${bold(inputFiles!)} → ${bold(files.join(', '))}...`)); } diff --git a/cli/run/watch-cli.ts b/cli/run/watch-cli.ts index fb2ef95de75..f6d33fc4fd4 100644 --- a/cli/run/watch-cli.ts +++ b/cli/run/watch-cli.ts @@ -106,9 +106,7 @@ export async function watch(command: any) { if (typeof input !== 'string') { input = Array.isArray(input) ? input.join(', ') - : Object.keys(input as Record) - .map(key => (input as Record)[key]) - .join(', '); + : Object.values(input as Record).join(', '); } stderr( cyan(`bundles ${bold(input)} → ${bold(event.output.map(relativeId).join(', '))}...`) diff --git a/src/Bundle.ts b/src/Bundle.ts index 01e731fa2a7..84b506f037d 100644 --- a/src/Bundle.ts +++ b/src/Bundle.ts @@ -6,6 +6,7 @@ import { GetManualChunk, NormalizedInputOptions, NormalizedOutputOptions, + OutputAsset, OutputBundle, OutputBundleWithPlaceholders, OutputChunk, @@ -41,11 +42,7 @@ export default class Bundle { async generate(isWrite: boolean): Promise { timeStart('GENERATE', 1); const outputBundle: OutputBundleWithPlaceholders = Object.create(null); - this.pluginDriver.setOutputBundle( - outputBundle, - this.outputOptions, - this.facadeChunkByModule - ); + this.pluginDriver.setOutputBundle(outputBundle, this.outputOptions, this.facadeChunkByModule); try { await this.pluginDriver.hookParallel('renderStart', [this.outputOptions, this.inputOptions]); @@ -104,9 +101,9 @@ export default class Bundle { ): Promise> { const manualChunkAliasByEntry = new Map(); const chunkEntries = await Promise.all( - Object.keys(manualChunks).map(async alias => ({ + Object.entries(manualChunks).map(async ([alias, files]) => ({ alias, - entries: await this.graph.moduleLoader.addAdditionalModules(manualChunks[alias]) + entries: await this.graph.moduleLoader.addAdditionalModules(files) })) ); for (const { alias, entries } of chunkEntries) { @@ -169,24 +166,23 @@ export default class Bundle { } private finaliseAssets(outputBundle: OutputBundleWithPlaceholders): void { - for (const key of Object.keys(outputBundle)) { - const file = outputBundle[key] as any; + for (const file of Object.values(outputBundle)) { if (!file.type) { warnDeprecation( 'A plugin is directly adding properties to the bundle object in the "generateBundle" hook. This is deprecated and will be removed in a future Rollup version, please use "this.emitFile" instead.', true, this.inputOptions ); - file.type = 'asset'; + (file as OutputAsset).type = 'asset'; } - if (this.outputOptions.validate && typeof file.code == 'string') { + if (this.outputOptions.validate && typeof (file as OutputChunk).code == 'string') { try { - this.graph.contextParse(file.code, { + this.graph.contextParse((file as OutputChunk).code, { allowHashBang: true, ecmaVersion: 'latest' }); } catch (exception) { - this.inputOptions.onwarn(errChunkInvalid(file, exception)); + this.inputOptions.onwarn(errChunkInvalid(file as OutputChunk, exception)); } } } diff --git a/src/Chunk.ts b/src/Chunk.ts index f8258da7b0c..cc03a6e8086 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -1326,8 +1326,8 @@ export default class Chunk { if (!this.outputOptions.preserveModules) { if (this.includedNamespaces.has(module)) { const memberVariables = module.namespace.getMemberVariables(); - for (const name of Object.keys(memberVariables)) { - moduleImports.add(memberVariables[name]); + for (const variable of Object.values(memberVariables)) { + moduleImports.add(variable); } } } diff --git a/src/Graph.ts b/src/Graph.ts index 9afb0fdda66..e62703cb37a 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -34,9 +34,9 @@ function normalizeEntryModules( name: null })); } - return Object.keys(entryModules).map(name => ({ + return Object.entries(entryModules).map(([name, id]) => ({ fileName: null, - id: entryModules[name], + id, implicitlyLoadedAfter: [], importer: undefined, name @@ -74,13 +74,14 @@ export default class Graph { // increment access counter for (const name in this.pluginCache) { const cache = this.pluginCache[name]; - for (const key of Object.keys(cache)) cache[key][0]++; + for (const value of Object.values(cache)) value[0]++; } } if (watcher) { this.watchMode = true; - const handleChange: WatchChangeHook = (...args) => this.pluginDriver.hookSeqSync('watchChange', args); + const handleChange: WatchChangeHook = (...args) => + this.pluginDriver.hookSeqSync('watchChange', args); const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); watcher.on('change', handleChange); watcher.on('close', handleClose); @@ -118,9 +119,9 @@ export default class Graph { if (onCommentOrig && typeof onCommentOrig == 'function') { options.onComment = (block, text, start, end, ...args) => { - comments.push({type: block ? "Block" : "Line", value: text, start, end}); + comments.push({ type: block ? 'Block' : 'Line', value: text, start, end }); return onCommentOrig.call(options, block, text, start, end, ...args); - } + }; } else { options.onComment = comments; } @@ -146,8 +147,8 @@ export default class Graph { for (const name in this.pluginCache) { const cache = this.pluginCache[name]; let allDeleted = true; - for (const key of Object.keys(cache)) { - if (cache[key][0] >= this.options.experimentalCacheExpiry) delete cache[key]; + for (const [key, value] of Object.entries(cache)) { + if (value[0] >= this.options.experimentalCacheExpiry) delete cache[key]; else allDeleted = false; } if (allDeleted) delete this.pluginCache[name]; @@ -238,8 +239,7 @@ export default class Graph { private warnForMissingExports() { for (const module of this.modules) { - for (const importName of Object.keys(module.importDescriptions)) { - const importDescription = module.importDescriptions[importName]; + for (const importDescription of Object.values(module.importDescriptions)) { if ( importDescription.name !== '*' && !(importDescription.module as Module).getVariableForExportName(importDescription.name) diff --git a/src/Module.ts b/src/Module.ts index 26d896a5901..db1f7eeb4d4 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -1002,8 +1002,7 @@ export default class Module { private addModulesToImportDescriptions(importDescription: { [name: string]: ImportDescription | ReexportDescription; }) { - for (const name of Object.keys(importDescription)) { - const specifier = importDescription[name]; + for (const specifier of Object.values(importDescription)) { const id = this.resolvedIds[specifier.source].id; specifier.module = this.graph.modulesById.get(id)!; } diff --git a/src/ast/keys.ts b/src/ast/keys.ts index 9434d9aae03..abd258e6e37 100644 --- a/src/ast/keys.ts +++ b/src/ast/keys.ts @@ -9,7 +9,7 @@ export const keys: { export function getAndCreateKeys(esTreeNode: GenericEsTreeNode) { keys[esTreeNode.type] = Object.keys(esTreeNode).filter( - key => key !== '_rollupAnnotations' && typeof esTreeNode[key] === 'object' + key => typeof esTreeNode[key] === 'object' && key !== '_rollupAnnotations' ); return keys[esTreeNode.type]; } diff --git a/src/ast/nodes/ArrowFunctionExpression.ts b/src/ast/nodes/ArrowFunctionExpression.ts index 81d920c7c5f..b9d1ba672f4 100644 --- a/src/ast/nodes/ArrowFunctionExpression.ts +++ b/src/ast/nodes/ArrowFunctionExpression.ts @@ -31,7 +31,6 @@ export default class ArrowFunctionExpression extends NodeBase { } } - // TODO Lukas handle nested paths and other event types // Arrow functions do not mutate their context deoptimizeThisOnEventAtPath() {} diff --git a/src/ast/nodes/AssignmentExpression.ts b/src/ast/nodes/AssignmentExpression.ts index 8517e48dacb..a515fc95009 100644 --- a/src/ast/nodes/AssignmentExpression.ts +++ b/src/ast/nodes/AssignmentExpression.ts @@ -122,7 +122,6 @@ export default class AssignmentExpression extends NodeBase { } } - // TODO Lukas is it time for propertyWriteSideEffects? protected applyDeoptimizations() { this.deoptimized = true; this.left.deoptimizePath(EMPTY_PATH); diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index ee6304f4396..c0d1087f1dc 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -122,7 +122,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } } - // TODO Lukas allow "event" to be a CallOptions object and get rid of thisParameter deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, diff --git a/src/ast/nodes/NewExpression.ts b/src/ast/nodes/NewExpression.ts index 54626a493c1..acff4ce3630 100644 --- a/src/ast/nodes/NewExpression.ts +++ b/src/ast/nodes/NewExpression.ts @@ -35,7 +35,6 @@ export default class NewExpression extends NodeBase { initialise() { this.callOptions = { args: this.arguments, - // TODO Lukas does it make sense, to use a pre-made object expression here? Can we get rid of "withNew"? thisParam: null, withNew: true }; diff --git a/src/ast/nodes/TaggedTemplateExpression.ts b/src/ast/nodes/TaggedTemplateExpression.ts index ef830dcfd5f..90a18c81ee2 100644 --- a/src/ast/nodes/TaggedTemplateExpression.ts +++ b/src/ast/nodes/TaggedTemplateExpression.ts @@ -52,7 +52,6 @@ export default class TaggedTemplateExpression extends NodeBase { initialise() { this.callOptions = { args: NO_ARGS, - // TODO Lukas verify thisParam: null, withNew: false, }; diff --git a/src/ast/nodes/shared/ClassNode.ts b/src/ast/nodes/shared/ClassNode.ts index ef21c7159e4..7a930b94582 100644 --- a/src/ast/nodes/shared/ClassNode.ts +++ b/src/ast/nodes/shared/ClassNode.ts @@ -21,14 +21,10 @@ import { ObjectEntity, ObjectProperty } from './ObjectEntity'; import { ObjectMember } from './ObjectMember'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; -// TODO Lukas -// * __proto__ assignment handling might be possible solely via the object prototype? But it would need to deoptimize the entire prototype chain: Bad. Better we always replace the prototype with "unknown" on assigment -// * __proto__: foo handling however is an ObjectExpression feature export default class ClassNode extends NodeBase implements DeoptimizableEntity { body!: ClassBody; id!: Identifier | null; superClass!: ExpressionNode | null; - private classConstructor!: MethodDefinition | null; private objectEntity: ObjectEntity | null = null; diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index a61462f9ba1..11c89253099 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -13,7 +13,6 @@ import { ObjectEntity } from './ObjectEntity'; import { OBJECT_PROTOTYPE } from './ObjectPrototype'; import { PatternNode } from './Pattern'; -// TODO Lukas improve prototype handling to fix #2219 export default class FunctionNode extends NodeBase { async!: boolean; body!: BlockStatement; @@ -21,7 +20,6 @@ export default class FunctionNode extends NodeBase { params!: PatternNode[]; preventChildBlockScope!: true; scope!: FunctionScope; - private isPrototypeDeoptimized = false; createScope(parentScope: FunctionScope) { @@ -42,7 +40,6 @@ export default class FunctionNode extends NodeBase { } } - // TODO Lukas handle other event types as well deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { if (event === EVENT_CALLED) { if (path.length > 0 ) { diff --git a/src/ast/nodes/shared/MethodBase.ts b/src/ast/nodes/shared/MethodBase.ts index 8976dbcb115..1a425f7cce5 100644 --- a/src/ast/nodes/shared/MethodBase.ts +++ b/src/ast/nodes/shared/MethodBase.ts @@ -22,7 +22,6 @@ export default class MethodBase extends NodeBase implements DeoptimizableEntity private accessedValue: ExpressionEntity | null = null; private accessorCallOptions: CallOptions = { args: NO_ARGS, - // TODO Lukas in the end, handle this differently or get rid of the shared call options thisParam: null, withNew: false }; diff --git a/src/ast/nodes/shared/MethodTypes.ts b/src/ast/nodes/shared/MethodTypes.ts index a5c20c4dac9..d43f4324ceb 100644 --- a/src/ast/nodes/shared/MethodTypes.ts +++ b/src/ast/nodes/shared/MethodTypes.ts @@ -76,7 +76,6 @@ export class Method extends ExpressionEntity { EMPTY_PATH, { args: NO_ARGS, - // TODO Lukas check if we need something else here thisParam: null, withNew: false }, diff --git a/src/ast/nodes/shared/Node.ts b/src/ast/nodes/shared/Node.ts index 72ce839432b..ccb0e0aa873 100644 --- a/src/ast/nodes/shared/Node.ts +++ b/src/ast/nodes/shared/Node.ts @@ -195,10 +195,9 @@ export class NodeBase extends ExpressionEntity implements ExpressionNode { } parseNode(esTreeNode: GenericEsTreeNode) { - for (const key of Object.keys(esTreeNode)) { + for (const [key, value] of Object.entries(esTreeNode)) { // That way, we can override this function to add custom initialisation and then call super.parseNode if (this.hasOwnProperty(key)) continue; - const value = esTreeNode[key]; if (key === '_rollupAnnotations') { this.annotations = value; } else if (typeof value !== 'object' || value === null) { diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 3ddea201473..53dfc4c3882 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -85,9 +85,9 @@ export class ObjectEntity extends ExpressionEntity { return; } this.hasUnknownDeoptimizedInteger = true; - for (const key of Object.keys(this.propertiesAndGettersByKey)) { + for (const [key, propertiesAndGetters] of Object.entries(this.propertiesAndGettersByKey)) { if (INTEGER_REG_EXP.test(key)) { - for (const property of this.propertiesAndGettersByKey[key]) { + for (const property of propertiesAndGetters) { property.deoptimizePath(UNKNOWN_PATH); } } @@ -421,7 +421,6 @@ export class ObjectEntity extends ExpressionEntity { } } - // TODO Lukas check everywhere if we can replace Object.keys with Object.entries/values private deoptimizeCachedIntegerEntities() { for (const [key, expressionsToBeDeoptimized] of Object.entries( this.expressionsToBeDeoptimizedByKey diff --git a/src/ast/variables/NamespaceVariable.ts b/src/ast/variables/NamespaceVariable.ts index c81cff49892..62bf7ac2b0e 100644 --- a/src/ast/variables/NamespaceVariable.ts +++ b/src/ast/variables/NamespaceVariable.ts @@ -66,9 +66,7 @@ export default class NamespaceVariable extends Variable { const t = options.indent; const memberVariables = this.getMemberVariables(); - const members = Object.keys(memberVariables).map(name => { - const original = memberVariables[name]; - + const members = Object.entries(memberVariables).map(([name, original]) => { if (this.referencedEarly || original.isReassigned) { return `${t}get ${name}${_}()${_}{${_}return ${original.getName()}${ options.compact ? '' : ';' diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 86519eb5439..b013af1c84f 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -170,9 +170,7 @@ async function handleGenerateWrite( message: 'You must specify "output.file" or "output.dir" for the build.' }); } - await Promise.all( - Object.keys(generated).map(chunkId => writeOutputFile(generated[chunkId], outputOptions)) - ); + await Promise.all(Object.values(generated).map(chunk => writeOutputFile(chunk, outputOptions))); await outputPluginDriver.hookParallel('writeBundle', [outputOptions, generated]); } return createOutput(generated); @@ -228,12 +226,9 @@ function getOutputOptions( function createOutput(outputBundle: Record): RollupOutput { return { - output: (Object.keys(outputBundle) - .map(fileName => outputBundle[fileName]) - .filter(outputFile => Object.keys(outputFile).length > 0) as ( - | OutputChunk - | OutputAsset - )[]).sort((outputFileA, outputFileB) => { + output: (Object.values(outputBundle).filter( + outputFile => Object.keys(outputFile).length > 0 + ) as (OutputChunk | OutputAsset)[]).sort((outputFileA, outputFileB) => { const fileTypeA = getSortingFileType(outputFileA); const fileTypeB = getSortingFileType(outputFileB); if (fileTypeA === fileTypeB) return 0; diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 65520b91fc3..8f0a3e1abd3 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -106,9 +106,12 @@ function hasValidType( ); } -function hasValidName(emittedFile: { type: 'asset' | 'chunk'; [key: string]: unknown; }): emittedFile is EmittedFile { +function hasValidName(emittedFile: { + type: 'asset' | 'chunk'; + [key: string]: unknown; +}): emittedFile is EmittedFile { const validatedName = emittedFile.fileName || emittedFile.name; - return !validatedName || typeof validatedName === 'string' && !isPathFragment(validatedName); + return !validatedName || (typeof validatedName === 'string' && !isPathFragment(validatedName)); } function getValidSource( @@ -351,8 +354,7 @@ function findExistingAssetFileNameWithSource( bundle: OutputBundleWithPlaceholders, source: string | Uint8Array ): string | null { - for (const fileName of Object.keys(bundle)) { - const outputFile = bundle[fileName]; + for (const [fileName, outputFile] of Object.entries(bundle)) { if (outputFile.type === 'asset' && areSourcesEqual(source, outputFile.source)) return fileName; } return null; diff --git a/src/utils/chunkAssignment.ts b/src/utils/chunkAssignment.ts index b4fb355377c..3490ddaf9ad 100644 --- a/src/utils/chunkAssignment.ts +++ b/src/utils/chunkAssignment.ts @@ -185,8 +185,8 @@ function createChunks( chunkModules[chunkSignature] = [module]; } } - return Object.keys(chunkModules).map(chunkSignature => ({ + return Object.values(chunkModules).map(modules => ({ alias: null, - modules: chunkModules[chunkSignature] + modules })); } diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index 649a10c6ff0..52626aff142 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -187,8 +187,8 @@ const getModuleContext = ( } if (configModuleContext) { const contextByModuleId = Object.create(null); - for (const key of Object.keys(configModuleContext)) { - contextByModuleId[resolve(key)] = configModuleContext[key]; + for (const [key, moduleContext] of Object.entries(configModuleContext)) { + contextByModuleId[resolve(key)] = moduleContext; } return id => contextByModuleId[id] || context; } diff --git a/src/utils/timers.ts b/src/utils/timers.ts index 5f030bbfafb..5ef2f77ba8d 100644 --- a/src/utils/timers.ts +++ b/src/utils/timers.ts @@ -76,8 +76,8 @@ function timeEndImpl(label: string, level = 3) { export function getTimings(): SerializedTimings { const newTimings: SerializedTimings = {}; - for (const label of Object.keys(timers)) { - newTimings[label] = [timers[label].time, timers[label].memory, timers[label].totalMemory]; + for (const [label, { time, memory, totalMemory }] of Object.entries(timers)) { + newTimings[label] = [time, memory, totalMemory]; } return newTimings; } @@ -102,7 +102,7 @@ function getPluginWithTimers(plugin: any, index: number): Plugin { timerLabel += ` (${plugin.name})`; } timerLabel += ` - ${hook}`; - timedPlugin[hook] = function() { + timedPlugin[hook] = function () { timeStart(timerLabel, 4); let result = plugin[hook].apply(this === timedPlugin ? plugin : this, arguments); timeEnd(timerLabel, 4); From 224a4e1c77a87b6525a2b1b23cf2ba15914a6424 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 16 May 2021 07:08:01 +0200 Subject: [PATCH 42/50] Improve code and simplify literal handling --- src/ast/nodes/Literal.ts | 2 +- src/ast/values.ts | 34 ++++++++----------- .../builtin-prototypes/literal/_expected.js | 9 +++++ .../builtin-prototypes/literal/main.js | 28 +++++---------- 4 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/ast/nodes/Literal.ts b/src/ast/nodes/Literal.ts index 5183108d398..d890950c2ea 100644 --- a/src/ast/nodes/Literal.ts +++ b/src/ast/nodes/Literal.ts @@ -62,7 +62,7 @@ export default class Literal extends Node context: HasEffectsContext ): boolean { if (path.length === 1) { - return hasMemberEffectWhenCalled(this.members, path[0], this.included, callOptions, context); + return hasMemberEffectWhenCalled(this.members, path[0], callOptions, context); } return true; } diff --git a/src/ast/values.ts b/src/ast/values.ts index 30866c41324..49cb6460626 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -6,7 +6,6 @@ import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { callsArgs: number[] | null; - mutatesSelf: boolean; returns: { new (): ExpressionEntity } | null; returnsPrimitive: ExpressionEntity | null; } @@ -35,7 +34,6 @@ export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpres const returnsUnknown: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_EXPRESSION } @@ -53,10 +51,13 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoole return path.length > 1; } - hasEffectsWhenCalledAtPath(path: ObjectPath) { + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalBooleanMembers[subPath]; + return hasMemberEffectWhenCalled(literalBooleanMembers, path[0], callOptions, context); } return true; } @@ -65,7 +66,6 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoole const returnsBoolean: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN } @@ -83,10 +83,13 @@ export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber return path.length > 1; } - hasEffectsWhenCalledAtPath(path: ObjectPath) { + hasEffectsWhenCalledAtPath( + path: ObjectPath, + callOptions: CallOptions, + context: HasEffectsContext + ) { if (path.length === 1) { - const subPath = path[0]; - return typeof subPath !== 'string' || !literalNumberMembers[subPath]; + return hasMemberEffectWhenCalled(literalNumberMembers, path[0], callOptions, context); } return true; } @@ -95,7 +98,6 @@ export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber const returnsNumber: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_NUMBER } @@ -119,7 +121,7 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString context: HasEffectsContext ) { if (path.length === 1) { - return hasMemberEffectWhenCalled(literalStringMembers, path[0], true, callOptions, context); + return hasMemberEffectWhenCalled(literalStringMembers, path[0], callOptions, context); } return true; } @@ -128,7 +130,6 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString const returnsString: RawMemberDescription = { value: { callsArgs: null, - mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_STRING } @@ -180,7 +181,6 @@ const literalStringMembers: MemberDescriptions = assembleMemberDescriptions( replace: { value: { callsArgs: [1], - mutatesSelf: false, returns: null, returnsPrimitive: UNKNOWN_LITERAL_STRING } @@ -217,16 +217,12 @@ export function getLiteralMembersForValue export function hasMemberEffectWhenCalled( members: MemberDescriptions, memberName: ObjectPathKey, - parentIncluded: boolean, callOptions: CallOptions, context: HasEffectsContext ) { - if ( - typeof memberName !== 'string' || - !members[memberName] || - (members[memberName].mutatesSelf && parentIncluded) - ) + if (typeof memberName !== 'string' || !members[memberName]) { return true; + } if (!members[memberName].callsArgs) return false; for (const argIndex of members[memberName].callsArgs!) { if ( diff --git a/test/form/samples/builtin-prototypes/literal/_expected.js b/test/form/samples/builtin-prototypes/literal/_expected.js index a2c2161b645..a3ef0d436f8 100644 --- a/test/form/samples/builtin-prototypes/literal/_expected.js +++ b/test/form/samples/builtin-prototypes/literal/_expected.js @@ -1,3 +1,12 @@ +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); // deep property access is forbidden diff --git a/test/form/samples/builtin-prototypes/literal/main.js b/test/form/samples/builtin-prototypes/literal/main.js index eaadeb2ba8c..2400111b686 100644 --- a/test/form/samples/builtin-prototypes/literal/main.js +++ b/test/form/samples/builtin-prototypes/literal/main.js @@ -4,25 +4,15 @@ const valueOf2 = true.valueOf(); const valueOf3 = true.valueOf().valueOf(); const valueOf4 = true.valueOf().valueOf().valueOf(); -const number = 1; -const toExponential1 = number.toExponential( 2 ); -const toExponential2 = (1).toExponential( 2 ); -const toExponential3 = (1).toExponential( 2 ).trim(); - -const string = ' b '; -const trim1 = string.trim(); -const trim2 = ' x '.trim(); -const trim3 = ' x '.trim().trim(); -const trim4 = ' x '.trim().trim().trim(); - -// boolean prototype -const _booleanValueOf = true.valueOf().valueOf(); -// inherited -const _booleanHasOwnProperty = true.hasOwnProperty( 'toString' ).valueOf(); -const _booleanIsPrototypeOf = true.isPrototypeOf( true ).valueOf(); -const _booleanPropertyIsEnumerable = true.propertyIsEnumerable( 'toString' ).valueOf(); -const _booleanToLocaleString = true.toLocaleString().trim(); -const _booleanToString = true.toString().trim(); +// retained +true.valueOf().unknown.unknown(); +true.valueOf()(); +(1).valueOf().unknown.unknown(); +(1).valueOf().unknown(); +(1).valueOf()[globalThis.unknown](); +(1).valueOf()(); +'ab'.charAt(1).unknown.unknown(); +'ab'.charAt(1)(); // number prototype const _toExponential = (1).toExponential( 2 ).trim(); From 87e8029fa06e87b001e2f47a879eb0e940691c45 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 16 May 2021 07:24:48 +0200 Subject: [PATCH 43/50] Improve coverage --- src/ast/nodes/CallExpression.ts | 24 +++---------------- .../builtin-prototypes/literal/_expected.js | 1 + .../builtin-prototypes/literal/main.js | 1 + 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/ast/nodes/CallExpression.ts b/src/ast/nodes/CallExpression.ts index c0d1087f1dc..a3dffadf3f5 100644 --- a/src/ast/nodes/CallExpression.ts +++ b/src/ast/nodes/CallExpression.ts @@ -46,7 +46,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt private deoptimizableDependentExpressions: DeoptimizableEntity[] = []; private expressionsToBeDeoptimized = new Set(); private returnExpression: ExpressionEntity | null = null; - private wasPathDeoptmizedWhileOptimized = false; bind() { super.bind(); @@ -86,23 +85,11 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt deoptimizeCache() { if (this.returnExpression !== UNKNOWN_EXPRESSION) { - this.returnExpression = null; - const returnExpression = this.getReturnExpression(); - const { deoptimizableDependentExpressions, expressionsToBeDeoptimized } = this; - if (returnExpression !== UNKNOWN_EXPRESSION) { - // We need to replace here because it is possible new expressions are added - // while we are deoptimizing the old ones - this.deoptimizableDependentExpressions = []; - this.expressionsToBeDeoptimized = new Set(); - if (this.wasPathDeoptmizedWhileOptimized) { - returnExpression.deoptimizePath(UNKNOWN_PATH); - this.wasPathDeoptmizedWhileOptimized = false; - } - } - for (const expression of deoptimizableDependentExpressions) { + this.returnExpression = UNKNOWN_EXPRESSION; + for (const expression of this.deoptimizableDependentExpressions) { expression.deoptimizeCache(); } - for (const expression of expressionsToBeDeoptimized) { + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizePath(UNKNOWN_PATH); } } @@ -117,7 +104,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } const returnExpression = this.getReturnExpression(); if (returnExpression !== UNKNOWN_EXPRESSION) { - this.wasPathDeoptmizedWhileOptimized = true; returnExpression.deoptimizePath(path); } } @@ -212,9 +198,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) { - return false; - } return ( !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && this.getReturnExpression().hasEffectsWhenAccessedAtPath(path, context) @@ -222,7 +205,6 @@ export default class CallExpression extends NodeBase implements DeoptimizableEnt } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; return ( !context.assigned.trackEntityAtPathAndGetIfTracked(path, this) && this.getReturnExpression().hasEffectsWhenAssignedAtPath(path, context) diff --git a/test/form/samples/builtin-prototypes/literal/_expected.js b/test/form/samples/builtin-prototypes/literal/_expected.js index a3ef0d436f8..75d80d652e5 100644 --- a/test/form/samples/builtin-prototypes/literal/_expected.js +++ b/test/form/samples/builtin-prototypes/literal/_expected.js @@ -7,6 +7,7 @@ true.valueOf()(); (1).valueOf()(); 'ab'.charAt(1).unknown.unknown(); 'ab'.charAt(1)(); +null.unknown; 'ab'.replace( 'a', () => console.log( 1 ) || 'b' ); // deep property access is forbidden diff --git a/test/form/samples/builtin-prototypes/literal/main.js b/test/form/samples/builtin-prototypes/literal/main.js index 2400111b686..e91c6a56144 100644 --- a/test/form/samples/builtin-prototypes/literal/main.js +++ b/test/form/samples/builtin-prototypes/literal/main.js @@ -13,6 +13,7 @@ true.valueOf()(); (1).valueOf()(); 'ab'.charAt(1).unknown.unknown(); 'ab'.charAt(1)(); +null.unknown; // number prototype const _toExponential = (1).toExponential( 2 ).trim(); From d2b61c9bb3de98cd65598541db628c74a9b66a0d Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 16 May 2021 07:59:53 +0200 Subject: [PATCH 44/50] Improve coverage --- src/ast/nodes/MemberExpression.ts | 10 ++------- src/ast/values.ts | 22 ++++++------------- .../_config.js | 4 ++++ .../_expected.js | 5 +++++ .../namespace-missing-export-effects/main.js | 7 ++++++ .../namespace-missing-export-effects/other.js | 0 6 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 test/form/samples/namespace-missing-export-effects/_config.js create mode 100644 test/form/samples/namespace-missing-export-effects/_expected.js create mode 100644 test/form/samples/namespace-missing-export-effects/main.js create mode 100644 test/form/samples/namespace-missing-export-effects/other.js diff --git a/src/ast/nodes/MemberExpression.ts b/src/ast/nodes/MemberExpression.ts index cb77210c11c..c5401379781 100644 --- a/src/ast/nodes/MemberExpression.ts +++ b/src/ast/nodes/MemberExpression.ts @@ -24,12 +24,7 @@ import Identifier from './Identifier'; import Literal from './Literal'; import * as NodeType from './NodeType'; import PrivateIdentifier from './PrivateIdentifier'; -import { - ExpressionEntity, - LiteralValueOrUnknown, - UnknownValue, - UNKNOWN_EXPRESSION -} from './shared/Expression'; +import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue, UNKNOWN_EXPRESSION } from './shared/Expression'; import { ExpressionNode, IncludeChildren, NodeBase } from './shared/Node'; import SpreadElement from './SpreadElement'; import Super from './Super'; @@ -211,12 +206,11 @@ export default class MemberExpression extends NodeBase implements DeoptimizableE } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; if (this.variable !== null) { return this.variable.hasEffectsWhenAccessedAtPath(path, context); } if (this.replacement) { - return false; + return true; } return this.object.hasEffectsWhenAccessedAtPath([this.getPropertyKey(), ...path], context); } diff --git a/src/ast/values.ts b/src/ast/values.ts index 49cb6460626..1edfae57d84 100644 --- a/src/ast/values.ts +++ b/src/ast/values.ts @@ -6,8 +6,7 @@ import { EMPTY_PATH, ObjectPath, ObjectPathKey } from './utils/PathTracker'; export interface MemberDescription { callsArgs: number[] | null; - returns: { new (): ExpressionEntity } | null; - returnsPrimitive: ExpressionEntity | null; + returns: ExpressionEntity; } export interface MemberDescriptions { @@ -34,8 +33,7 @@ export const UNDEFINED_EXPRESSION: ExpressionEntity = new (class UndefinedExpres const returnsUnknown: RawMemberDescription = { value: { callsArgs: null, - returns: null, - returnsPrimitive: UNKNOWN_EXPRESSION + returns: UNKNOWN_EXPRESSION } }; @@ -66,8 +64,7 @@ export const UNKNOWN_LITERAL_BOOLEAN: ExpressionEntity = new (class UnknownBoole const returnsBoolean: RawMemberDescription = { value: { callsArgs: null, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_BOOLEAN + returns: UNKNOWN_LITERAL_BOOLEAN } }; @@ -98,8 +95,7 @@ export const UNKNOWN_LITERAL_NUMBER: ExpressionEntity = new (class UnknownNumber const returnsNumber: RawMemberDescription = { value: { callsArgs: null, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_NUMBER + returns: UNKNOWN_LITERAL_NUMBER } }; @@ -130,8 +126,7 @@ export const UNKNOWN_LITERAL_STRING: ExpressionEntity = new (class UnknownString const returnsString: RawMemberDescription = { value: { callsArgs: null, - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_STRING } }; @@ -181,8 +176,7 @@ const literalStringMembers: MemberDescriptions = assembleMemberDescriptions( replace: { value: { callsArgs: [1], - returns: null, - returnsPrimitive: UNKNOWN_LITERAL_STRING + returns: UNKNOWN_LITERAL_STRING } }, search: returnsNumber, @@ -247,7 +241,5 @@ export function getMemberReturnExpressionWhenCalled( memberName: ObjectPathKey ): ExpressionEntity { if (typeof memberName !== 'string' || !members[memberName]) return UNKNOWN_EXPRESSION; - return members[memberName].returnsPrimitive !== null - ? members[memberName].returnsPrimitive! - : new members[memberName].returns!(); + return members[memberName].returns; } diff --git a/test/form/samples/namespace-missing-export-effects/_config.js b/test/form/samples/namespace-missing-export-effects/_config.js new file mode 100644 index 00000000000..87a35ab8485 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'handles interacting with missing namespace members', + expectedWarnings: ['MISSING_EXPORT'] +}; diff --git a/test/form/samples/namespace-missing-export-effects/_expected.js b/test/form/samples/namespace-missing-export-effects/_expected.js new file mode 100644 index 00000000000..8895a217864 --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/_expected.js @@ -0,0 +1,5 @@ +if (!undefined) console.log(1); +if (undefined()) console.log(2); +const foo = undefined; +foo.bar; +(0, undefined)(); diff --git a/test/form/samples/namespace-missing-export-effects/main.js b/test/form/samples/namespace-missing-export-effects/main.js new file mode 100644 index 00000000000..e37bad3f8ac --- /dev/null +++ b/test/form/samples/namespace-missing-export-effects/main.js @@ -0,0 +1,7 @@ +import * as ns from './other.js'; + +if (!ns.foo) console.log(1); +if (ns.foo()) console.log(2); +const foo = ns.foo; +foo.bar; +(true && ns.foo)(); diff --git a/test/form/samples/namespace-missing-export-effects/other.js b/test/form/samples/namespace-missing-export-effects/other.js new file mode 100644 index 00000000000..e69de29bb2d From 66a5cf9985097844983cce4259e59866ed69a460 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 16 May 2021 19:31:55 +0200 Subject: [PATCH 45/50] Improve coverage in conditional and logical expressions --- src/ast/nodes/ConditionalExpression.ts | 29 +++++++------------ src/ast/nodes/LogicalExpression.ts | 26 +++++------------ .../conditionals-deoptimization/_config.js | 8 +++++ .../conditionals-deoptimization/main.js | 21 ++++++++++++++ 4 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 test/function/samples/conditionals-deoptimization/_config.js create mode 100644 test/function/samples/conditionals-deoptimization/main.js diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index 8258283c2ff..b8ba4a24c05 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -35,36 +35,31 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; + //TODO Lukas check if propertyWriteSideEffects would make sense to prevent hasEffectsWhenAssigned + // TODO Lukas check if propertyReadSideEffects also prevents this mutation deoptimizeCache() { if (this.usedBranch !== null) { const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - unusedBranch.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } } deoptimizePath(path: ObjectPath) { - if (path.length > 0) { - const usedBranch = this.getUsedBranch(); - if (usedBranch === null) { - this.consequent.deoptimizePath(path); - this.alternate.deoptimizePath(path); - } else { - this.wasPathDeoptimizedWhileOptimized = true; - usedBranch.deoptimizePath(path); - } + const usedBranch = this.getUsedBranch(); + if (usedBranch === null) { + this.consequent.deoptimizePath(path); + this.alternate.deoptimizePath(path); + } else { + usedBranch.deoptimizePath(path); } } + // TODO Lukas other events? And is the given event even relevant as we will forget "this" anyway? deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, @@ -129,7 +124,6 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( @@ -141,7 +135,6 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index 385861a406c..ddf05d53d54 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -36,19 +36,14 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable // We collect deoptimization information if usedBranch !== null private expressionsToBeDeoptimized: DeoptimizableEntity[] = []; private isBranchResolutionAnalysed = false; - private unusedBranch: ExpressionNode | null = null; private usedBranch: ExpressionNode | null = null; - private wasPathDeoptimizedWhileOptimized = false; deoptimizeCache() { if (this.usedBranch !== null) { + const unusedBranch = this.usedBranch === this.left ? this.right : this.left; this.usedBranch = null; - const expressionsToBeDeoptimized = this.expressionsToBeDeoptimized; - this.expressionsToBeDeoptimized = []; - if (this.wasPathDeoptimizedWhileOptimized) { - this.unusedBranch!.deoptimizePath(UNKNOWN_PATH); - } - for (const expression of expressionsToBeDeoptimized) { + unusedBranch.deoptimizePath(UNKNOWN_PATH); + for (const expression of this.expressionsToBeDeoptimized) { expression.deoptimizeCache(); } } @@ -60,11 +55,11 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable this.left.deoptimizePath(path); this.right.deoptimizePath(path); } else { - this.wasPathDeoptimizedWhileOptimized = true; usedBranch.deoptimizePath(path); } } + // TODO Lukas other events? And is the given event even relevant? deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, @@ -120,7 +115,6 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return false; const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( @@ -132,7 +126,6 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } hasEffectsWhenAssignedAtPath(path: ObjectPath, context: HasEffectsContext): boolean { - if (path.length === 0) return true; const usedBranch = this.getUsedBranch(); if (usedBranch === null) { return ( @@ -214,17 +207,12 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable if (leftValue === UnknownValue) { return null; } else { - if ( + this.usedBranch = (this.operator === '||' && leftValue) || (this.operator === '&&' && !leftValue) || (this.operator === '??' && leftValue != null) - ) { - this.usedBranch = this.left; - this.unusedBranch = this.right; - } else { - this.usedBranch = this.right; - this.unusedBranch = this.left; - } + ? this.left + : this.right; } } return this.usedBranch; diff --git a/test/function/samples/conditionals-deoptimization/_config.js b/test/function/samples/conditionals-deoptimization/_config.js new file mode 100644 index 00000000000..48ed684f81d --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/_config.js @@ -0,0 +1,8 @@ +const assert = require('assert'); + +module.exports = { + description: 'handles deoptimization of conditionals', + exports(exports) { + assert.deepStrictEqual(exports, { first: true, second: true, third: true, fourth: true }); + } +}; diff --git a/test/function/samples/conditionals-deoptimization/main.js b/test/function/samples/conditionals-deoptimization/main.js new file mode 100644 index 00000000000..324e03b344e --- /dev/null +++ b/test/function/samples/conditionals-deoptimization/main.js @@ -0,0 +1,21 @@ +export let first = false; +export let second = false; +export let third = false; +export let fourth = false; + +let flag = false; +checkConditional(); +checkLogical(); +flag = true; +checkConditional(); +checkLogical(); + +function checkConditional() { + if (flag ? true : false) first = true; + else second = true; +} + +function checkLogical() { + if (flag && true) third = true; + else fourth = true; +} From 76b5cbe197efdbbbb692a5ef28fbbaf5adb7e6d6 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 17 May 2021 07:28:01 +0200 Subject: [PATCH 46/50] Improve coverage --- src/ast/nodes/ArrayExpression.ts | 4 ---- src/ast/nodes/ConditionalExpression.ts | 19 ++++----------- src/ast/nodes/Identifier.ts | 3 +-- src/ast/nodes/LogicalExpression.ts | 9 +++----- src/ast/nodes/shared/FunctionNode.ts | 1 + src/ast/variables/LocalVariable.ts | 9 +++----- src/ast/variables/ThisVariable.ts | 23 +------------------ .../samples/deoptimize-class/_config.js | 3 +++ .../function/samples/deoptimize-class/main.js | 20 ++++++++++++++++ .../samples/deoptimize-object/_config.js | 3 +++ .../samples/deoptimize-object/main.js | 13 +++++++++++ .../deoptimize-this-parameters/_config.js | 3 +++ .../deoptimize-this-parameters/main.js | 21 +++++++++++++++++ 13 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 test/function/samples/deoptimize-class/_config.js create mode 100644 test/function/samples/deoptimize-class/main.js create mode 100644 test/function/samples/deoptimize-object/_config.js create mode 100644 test/function/samples/deoptimize-object/main.js create mode 100644 test/function/samples/deoptimize-this-parameters/_config.js create mode 100644 test/function/samples/deoptimize-this-parameters/main.js diff --git a/src/ast/nodes/ArrayExpression.ts b/src/ast/nodes/ArrayExpression.ts index db560d8ee52..34c24bef3f6 100644 --- a/src/ast/nodes/ArrayExpression.ts +++ b/src/ast/nodes/ArrayExpression.ts @@ -16,10 +16,6 @@ export default class ArrayExpression extends NodeBase { type!: NodeType.tArrayExpression; private objectEntity: ObjectEntity | null = null; - deoptimizeCache() { - this.getObjectEntity().deoptimizeAllProperties(); - } - deoptimizePath(path: ObjectPath) { this.getObjectEntity().deoptimizePath(path); } diff --git a/src/ast/nodes/ConditionalExpression.ts b/src/ast/nodes/ConditionalExpression.ts index b8ba4a24c05..d11b2869319 100644 --- a/src/ast/nodes/ConditionalExpression.ts +++ b/src/ast/nodes/ConditionalExpression.ts @@ -11,14 +11,8 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; -import { - EMPTY_PATH, - ObjectPath, - PathTracker, - SHARED_RECURSION_TRACKER, - UNKNOWN_PATH -} from '../utils/PathTracker'; +import { NodeEvent } from '../NodeEvents'; +import { EMPTY_PATH, ObjectPath, PathTracker, SHARED_RECURSION_TRACKER, UNKNOWN_PATH } from '../utils/PathTracker'; import CallExpression from './CallExpression'; import * as NodeType from './NodeType'; import { ExpressionEntity, LiteralValueOrUnknown, UnknownValue } from './shared/Expression'; @@ -36,8 +30,6 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz private isBranchResolutionAnalysed = false; private usedBranch: ExpressionNode | null = null; - //TODO Lukas check if propertyWriteSideEffects would make sense to prevent hasEffectsWhenAssigned - // TODO Lukas check if propertyReadSideEffects also prevents this mutation deoptimizeCache() { if (this.usedBranch !== null) { const unusedBranch = this.usedBranch === this.consequent ? this.alternate : this.consequent; @@ -59,17 +51,14 @@ export default class ConditionalExpression extends NodeBase implements Deoptimiz } } - // TODO Lukas other events? And is the given event even relevant as we will forget "this" anyway? deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - if (event === EVENT_CALLED || path.length > 0) { - this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - } + this.consequent.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.alternate.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); } getLiteralValueAtPath( diff --git a/src/ast/nodes/Identifier.ts b/src/ast/nodes/Identifier.ts index 6ce39e6cbdb..6fabee91ebf 100644 --- a/src/ast/nodes/Identifier.ts +++ b/src/ast/nodes/Identifier.ts @@ -178,8 +178,7 @@ export default class Identifier extends NodeBase implements PatternNode { this.deoptimized = true; if ( this.variable !== null && - this.variable instanceof LocalVariable && - this.variable.additionalInitializers !== null + this.variable instanceof LocalVariable ) { this.variable.consolidateInitializers(); } diff --git a/src/ast/nodes/LogicalExpression.ts b/src/ast/nodes/LogicalExpression.ts index ddf05d53d54..9faf815cdcd 100644 --- a/src/ast/nodes/LogicalExpression.ts +++ b/src/ast/nodes/LogicalExpression.ts @@ -11,7 +11,7 @@ import { removeAnnotations } from '../../utils/treeshakeNode'; import { CallOptions } from '../CallOptions'; import { DeoptimizableEntity } from '../DeoptimizableEntity'; import { HasEffectsContext, InclusionContext } from '../ExecutionContext'; -import { EVENT_CALLED, NodeEvent } from '../NodeEvents'; +import { NodeEvent } from '../NodeEvents'; import { EMPTY_PATH, ObjectPath, @@ -59,17 +59,14 @@ export default class LogicalExpression extends NodeBase implements Deoptimizable } } - // TODO Lukas other events? And is the given event even relevant? deoptimizeThisOnEventAtPath( event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity, recursionTracker: PathTracker ) { - if (event === EVENT_CALLED || path.length > 0) { - this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); - } + this.left.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); + this.right.deoptimizeThisOnEventAtPath(event, path, thisParameter, recursionTracker); } getLiteralValueAtPath( diff --git a/src/ast/nodes/shared/FunctionNode.ts b/src/ast/nodes/shared/FunctionNode.ts index 11c89253099..3f7cf1f66a5 100644 --- a/src/ast/nodes/shared/FunctionNode.ts +++ b/src/ast/nodes/shared/FunctionNode.ts @@ -40,6 +40,7 @@ export default class FunctionNode extends NodeBase { } } + // TODO for completeness, we should also track other events here deoptimizeThisOnEventAtPath(event: NodeEvent, path: ObjectPath, thisParameter: ExpressionEntity) { if (event === EVENT_CALLED) { if (path.length > 0 ) { diff --git a/src/ast/variables/LocalVariable.ts b/src/ast/variables/LocalVariable.ts index 92573a8a3c7..346d1687487 100644 --- a/src/ast/variables/LocalVariable.ts +++ b/src/ast/variables/LocalVariable.ts @@ -82,12 +82,10 @@ export default class LocalVariable extends Variable { for (const expression of expressionsToBeDeoptimized) { expression.deoptimizeCache(); } - if (this.init) { - this.init.deoptimizePath(UNKNOWN_PATH); - } + this.init?.deoptimizePath(UNKNOWN_PATH); } - } else if (this.init) { - this.init.deoptimizePath(path); + } else { + this.init?.deoptimizePath(path); } } @@ -153,7 +151,6 @@ export default class LocalVariable extends Variable { } hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { - if (path.length === 0) return false; if (this.isReassigned || path.length > MAX_PATH_DEPTH) return true; return (this.init && !context.accessed.trackEntityAtPathAndGetIfTracked(path, this) && diff --git a/src/ast/variables/ThisVariable.ts b/src/ast/variables/ThisVariable.ts index 5ca668c454d..76ba2127b5a 100644 --- a/src/ast/variables/ThisVariable.ts +++ b/src/ast/variables/ThisVariable.ts @@ -1,13 +1,7 @@ import { AstContext } from '../../Module'; -import { CallOptions } from '../CallOptions'; import { HasEffectsContext } from '../ExecutionContext'; import { NodeEvent } from '../NodeEvents'; -import { - ExpressionEntity, - LiteralValueOrUnknown, - UnknownValue, - UNKNOWN_EXPRESSION -} from '../nodes/shared/Expression'; +import { ExpressionEntity, UNKNOWN_EXPRESSION } from '../nodes/shared/Expression'; import { ObjectPath, SHARED_RECURSION_TRACKER } from '../utils/PathTracker'; import LocalVariable from './LocalVariable'; @@ -61,10 +55,6 @@ export default class ThisVariable extends LocalVariable { this.thisDeoptimizations.push(thisDeoptimization); } - getLiteralValueAtPath(): LiteralValueOrUnknown { - return UnknownValue; - } - hasEffectsWhenAccessedAtPath(path: ObjectPath, context: HasEffectsContext) { return ( this.getInit(context).hasEffectsWhenAccessedAtPath(path, context) || @@ -79,17 +69,6 @@ export default class ThisVariable extends LocalVariable { ); } - hasEffectsWhenCalledAtPath( - path: ObjectPath, - callOptions: CallOptions, - context: HasEffectsContext - ) { - return ( - this.getInit(context).hasEffectsWhenCalledAtPath(path, callOptions, context) || - super.hasEffectsWhenCalledAtPath(path, callOptions, context) - ); - } - private applyThisDeoptimizationEvent( entity: ExpressionEntity, { event, path, thisParameter }: ThisDeoptimizationEvent diff --git a/test/function/samples/deoptimize-class/_config.js b/test/function/samples/deoptimize-class/_config.js new file mode 100644 index 00000000000..14bcdb722ee --- /dev/null +++ b/test/function/samples/deoptimize-class/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of static class properties' +}; diff --git a/test/function/samples/deoptimize-class/main.js b/test/function/samples/deoptimize-class/main.js new file mode 100644 index 00000000000..f67b5d66413 --- /dev/null +++ b/test/function/samples/deoptimize-class/main.js @@ -0,0 +1,20 @@ +let trueProp = 'old'; +let falseProp = 'new'; + +function getClass() { + return class { + static [trueProp]() { + return true; + } + static [falseProp]() { + return false; + } + }; +} + +if (!getClass().old()) throw new Error('old missing'); +if (getClass().new()) throw new Error('new present'); +trueProp = 'new'; +falseProp = 'old'; +if (getClass().old()) throw new Error('old present'); +if (!getClass().new()) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-object/_config.js b/test/function/samples/deoptimize-object/_config.js new file mode 100644 index 00000000000..2906b6780d7 --- /dev/null +++ b/test/function/samples/deoptimize-object/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of object properties' +}; diff --git a/test/function/samples/deoptimize-object/main.js b/test/function/samples/deoptimize-object/main.js new file mode 100644 index 00000000000..bfd1b80873e --- /dev/null +++ b/test/function/samples/deoptimize-object/main.js @@ -0,0 +1,13 @@ +let prop = 'old'; + +function getObj() { + return { + [prop]: true + }; +} + +if (!getObj().old) throw new Error('old missing'); +if (getObj().new) throw new Error('new present'); +prop = 'new'; +if (getObj().old) throw new Error('old present'); +if (!getObj().new) throw new Error('new missing'); diff --git a/test/function/samples/deoptimize-this-parameters/_config.js b/test/function/samples/deoptimize-this-parameters/_config.js new file mode 100644 index 00000000000..518f65d290a --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'handles deoptimization of this parameters' +}; diff --git a/test/function/samples/deoptimize-this-parameters/main.js b/test/function/samples/deoptimize-this-parameters/main.js new file mode 100644 index 00000000000..89792dd4aea --- /dev/null +++ b/test/function/samples/deoptimize-this-parameters/main.js @@ -0,0 +1,21 @@ +const obj = { + flag: false, + otherFlag: false, + otherProp() { + this.otherFlag = true; + }, + prop() { + this.flag = true; + this.otherProp(); + } +}; + +const otherObj = { + prop: obj.prop, + otherProp: obj.otherProp +}; + +obj.prop(); +otherObj.prop(); +if (!obj.flag) throw new Error('first flag missing'); +if (!otherObj.flag) throw new Error('second flag missing'); From b6bd8d4d990b5e9a2c0ba62714919a1fc26dc441 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 18 May 2021 06:20:53 +0200 Subject: [PATCH 47/50] 2.49.0-0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0830654b069..5fa5f5f308d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cb616b25548..b807083b977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.48.0", + "version": "2.49.0-0", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", From 99964aa9564cb8a1641b408bd5e399d2d6c2d91d Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 18 May 2021 06:31:25 +0200 Subject: [PATCH 48/50] Fix test to support pre-release versions --- test/cli/samples/watch/watch-config-no-update/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/samples/watch/watch-config-no-update/_config.js b/test/cli/samples/watch/watch-config-no-update/_config.js index c93075b0cc3..635d77d42d9 100644 --- a/test/cli/samples/watch/watch-config-no-update/_config.js +++ b/test/cli/samples/watch/watch-config-no-update/_config.js @@ -29,7 +29,7 @@ module.exports = { }, stderr(stderr) { if ( - !/^rollup v\d+\.\d+\.\d+\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( + !/^rollup v\d+\.\d+\.\d+(-\d+)?\nbundles main.js → _actual[\\/]main.js...\ncreated _actual[\\/]main.js in \d+ms\n$/.test( stderr ) ) { From 84b0d94cf1914e021ed7657d4605fd568723aede Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 20 May 2021 13:41:45 +0200 Subject: [PATCH 49/50] Fix failed deoptimization of array props --- src/ast/nodes/shared/ObjectEntity.ts | 1 + test/function/samples/array-mutation/_config.js | 3 +++ test/function/samples/array-mutation/main.js | 4 ++++ 3 files changed, 8 insertions(+) create mode 100644 test/function/samples/array-mutation/_config.js create mode 100644 test/function/samples/array-mutation/main.js diff --git a/src/ast/nodes/shared/ObjectEntity.ts b/src/ast/nodes/shared/ObjectEntity.ts index 53dfc4c3882..b754c36413f 100644 --- a/src/ast/nodes/shared/ObjectEntity.ts +++ b/src/ast/nodes/shared/ObjectEntity.ts @@ -440,6 +440,7 @@ export class ObjectEntity extends ExpressionEntity { if ( this.hasUnknownDeoptimizedProperty || typeof key !== 'string' || + (this.hasUnknownDeoptimizedInteger && INTEGER_REG_EXP.test(key)) || this.deoptimizedPaths[key] ) { return UNKNOWN_EXPRESSION; diff --git a/test/function/samples/array-mutation/_config.js b/test/function/samples/array-mutation/_config.js new file mode 100644 index 00000000000..612a42d2e21 --- /dev/null +++ b/test/function/samples/array-mutation/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'tracks array mutations' +}; diff --git a/test/function/samples/array-mutation/main.js b/test/function/samples/array-mutation/main.js new file mode 100644 index 00000000000..98363f0f469 --- /dev/null +++ b/test/function/samples/array-mutation/main.js @@ -0,0 +1,4 @@ +const array = []; +array.push(true); + +assert.strictEqual(array[0] || false, true); From 7490a87411e919cece4b863ff6332354c8e1739c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 20 May 2021 13:43:07 +0200 Subject: [PATCH 50/50] 2.49.0-1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fa5f5f308d..a3f12ec6d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.49.0-0", + "version": "2.49.0-1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b807083b977..cbb4e3f6b96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.49.0-0", + "version": "2.49.0-1", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js",