Optimize Object.assign calls by merging the temporal entries together where possible (#2206)

Summary:
Release notes: adds an optimization to Object.assign that attempts to merge calls together where possible

I frequently see `Objet.assign` calls litter our internal bundle. Many of them can actually be merged together to reduce bloat and runtime overhead (on that matter, `Object.assign` isn't a cheap call). We do this by adding a new `TemporalObjectAssignEntry` entry, that allows us to add some fine-tuning of Object.assign temporal entries we create.

The new `TemporalObjectAssignEntry` entry has an optimization that aims to merge entries by checking if linked nodes, specifically the `Object.assign` calls `to` field, to see if it can literally merge the arguments of many `Object.assign`s together. If a `to` is visited and can't be omitted, it doesn't try to apply this optimization. What we end up with, is a single `Object.assign` call and the others all get dead-code eliminated away because of the `mutatesOnly` logic from earlier.
Pull Request resolved: https://github.com/facebook/prepack/pull/2206

Reviewed By: NTillmann

Differential Revision: D8775391

Pulled By: trueadm

fbshipit-source-id: 41e5d6bb1d51fcfff66b2fe758bd51492ec472d9
This commit is contained in:
Dominic Gannaway 2018-07-09 16:46:51 -07:00 committed by Facebook Github Bot
parent dd1f18da0c
commit db5ed0c7d3
33 changed files with 688 additions and 77 deletions

View File

@ -520,9 +520,8 @@ export default function(realm: Realm): ObjectValue {
let unfiltered;
if (text instanceof AbstractValue && text.kind === "JSON.stringify(...)") {
// Enable cloning via JSON.parse(JSON.stringify(...)).
let gen = realm.preludeGenerator;
invariant(gen); // text is abstract, so we are doing abstract interpretation
let temporalBuildNodeEntryArgs = gen.derivedIds.get(text.intrinsicName);
// text is abstract, so we are doing abstract interpretation
let temporalBuildNodeEntryArgs = realm.derivedIds.get(text.intrinsicName);
invariant(temporalBuildNodeEntryArgs !== undefined);
let args = temporalBuildNodeEntryArgs.args;
invariant(args[0] instanceof Value); // since text.kind === "JSON.stringify(...)"

View File

@ -100,7 +100,7 @@ function copyKeys(realm: Realm, keys, from, to): void {
function applyObjectAssignSource(
realm: Realm,
nextSource: ObjectValue | AbstractObjectValue,
nextSource: Value,
to: ObjectValue | AbstractObjectValue,
delayedSources: Array<Value>,
to_must_be_partial: boolean
@ -152,7 +152,7 @@ function applyObjectAssignSource(
function tryAndApplySourceOrRecover(
realm: Realm,
nextSource: ObjectValue | AbstractObjectValue,
nextSource: Value,
to: ObjectValue | AbstractObjectValue,
delayedSources: Array<Value>,
to_must_be_partial: boolean
@ -225,7 +225,7 @@ export default function(realm: Realm): NativeFunctionValue {
// ECMA262 19.1.2.1
if (!realm.isCompatibleWith(realm.MOBILE_JSC_VERSION) && !realm.isCompatibleWith("mobile")) {
let ObjectAssign = func.defineNativeMethod("assign", 2, (context, [target, ...sources]) => {
func.defineNativeMethod("assign", 2, (context, [target, ...sources]) => {
// 1. Let to be ? ToObject(target).
let to = To.ToObject(realm, target);
let to_must_be_partial = false;
@ -274,17 +274,20 @@ export default function(realm: Realm): NativeFunctionValue {
to.makeSimple();
// Tell serializer that it may add properties to to only after temporalTo has been emitted
let temporalArgs = [ObjectAssign, to, ...delayedSources];
let temporalArgs = [to, ...delayedSources];
let preludeGenerator = realm.preludeGenerator;
invariant(preludeGenerator !== undefined);
let temporalTo = AbstractValue.createTemporalFromBuildFunction(
realm,
ObjectValue,
temporalArgs,
([methodNode, targetNode, ...sourceNodes]: Array<BabelNodeExpression>) => {
return t.callExpression(methodNode, [targetNode, ...sourceNodes]);
([targetNode, ...sourceNodes]: Array<BabelNodeExpression>) => {
return t.callExpression(preludeGenerator.memoizeReference("Object.assign"), [targetNode, ...sourceNodes]);
},
{
skipInvariant: true,
mutatesOnly: [to],
temporalType: "OBJECT_ASSIGN",
}
);
invariant(temporalTo instanceof AbstractObjectValue);

View File

@ -143,12 +143,12 @@ export class ReactEquivalenceSet {
if (!this.residualReactElementVisitor.wasTemporalAliasDeclaredInCurrentScope(temporalAlias)) {
return temporalAlias;
}
let temporalBuildNodeEntryArgs = this.realm.getTemporalBuildNodeEntryArgsFromDerivedValue(temporalAlias);
let temporalBuildNodeEntry = this.realm.getTemporalBuildNodeEntryFromDerivedValue(temporalAlias);
if (temporalBuildNodeEntryArgs === undefined) {
if (temporalBuildNodeEntry === undefined) {
return temporalAlias;
}
let temporalArgs = temporalBuildNodeEntryArgs.args;
let temporalArgs = temporalBuildNodeEntry.args;
if (temporalArgs.length === 0) {
return temporalAlias;
}
@ -169,9 +169,9 @@ export class ReactEquivalenceSet {
}
} else if (arg instanceof AbstractObjectValue && !arg.values.isTop() && arg.kind !== "conditional") {
// Might be a temporal, so let's check
let childTemporalBuildNodeEntryArgs = this.realm.getTemporalBuildNodeEntryArgsFromDerivedValue(arg);
let childTemporalBuildNodeEntry = this.realm.getTemporalBuildNodeEntryFromDerivedValue(arg);
if (childTemporalBuildNodeEntryArgs !== undefined) {
if (childTemporalBuildNodeEntry !== undefined) {
equivalenceArg = this._getTemporalValue(arg, visitedValues);
invariant(equivalenceArg instanceof AbstractObjectValue);

View File

@ -19,7 +19,6 @@ import {
NativeFunctionValue,
ECMAScriptFunctionValue,
Value,
FunctionValue,
} from "../values/index.js";
import * as t from "babel-types";
import type { BabelNodeIdentifier } from "babel-types";
@ -370,18 +369,20 @@ export function applyGetDerivedStateFromProps(
let c = AbstractValue.createFromLogicalOp(realm, "&&", a, b);
invariant(c instanceof AbstractValue);
let newState = new ObjectValue(realm, realm.intrinsics.ObjectPrototype);
let preludeGenerator = realm.preludeGenerator;
invariant(preludeGenerator !== undefined);
// we cannot use the standard Object.assign as partial state
// is not simple. however, given getDerivedStateFromProps is
// meant to be pure, we can assume that there are no getters on
// the partial abstract state
AbstractValue.createTemporalFromBuildFunction(
realm,
FunctionValue,
[objectAssign, newState, prevState, state],
([methodNode, ..._args]) => {
return t.callExpression(methodNode, ((_args: any): Array<any>));
ObjectValue,
[newState, prevState, state],
([..._args]) => {
return t.callExpression(preludeGenerator.memoizeReference("Object.assign"), ((_args: any): Array<any>));
},
{ skipInvariant: true }
{ skipInvariant: true, mutatesOnly: [newState], temporalType: "OBJECT_ASSIGN" }
);
newState.makeSimple();
newState.makePartial();
@ -395,14 +396,20 @@ export function applyGetDerivedStateFromProps(
objectAssignCall(realm.intrinsics.undefined, [newState, prevState, state]);
} catch (e) {
if (realm.isInPureScope() && e instanceof FatalError) {
let preludeGenerator = realm.preludeGenerator;
invariant(preludeGenerator !== undefined);
AbstractValue.createTemporalFromBuildFunction(
realm,
FunctionValue,
ObjectValue,
[objectAssign, newState, prevState, state],
([methodNode, ..._args]) => {
return t.callExpression(methodNode, ((_args: any): Array<any>));
([..._args]) => {
return t.callExpression(preludeGenerator.memoizeReference("Object.assign"), ((_args: any): Array<any>));
},
{ skipInvariant: true, mutatesOnly: [newState] }
{
skipInvariant: true,
mutatesOnly: [newState],
temporalType: "OBJECT_ASSIGN",
}
);
newState.makeSimple();
newState.makePartial();

View File

@ -30,7 +30,7 @@ import {
UndefinedValue,
Value,
} from "../values/index.js";
import { Generator } from "../utils/generator.js";
import { Generator, TemporalObjectAssignEntry } from "../utils/generator.js";
import type {
Descriptor,
FunctionBodyAstNode,
@ -944,20 +944,30 @@ function applyClonedTemporalAlias(realm: Realm, props: ObjectValue, clonedProps:
// be a better option.
invariant(false, "TODO applyClonedTemporalAlias conditional");
}
let temporalBuildNodeEntryArgs = realm.getTemporalBuildNodeEntryArgsFromDerivedValue(temporalAlias);
invariant(temporalBuildNodeEntryArgs !== undefined);
let temporalArgs = temporalBuildNodeEntryArgs.args;
let temporalBuildNodeEntry = realm.getTemporalBuildNodeEntryFromDerivedValue(temporalAlias);
if (!(temporalBuildNodeEntry instanceof TemporalObjectAssignEntry)) {
invariant(false, "TODO nont TemporalObjectAssignEntry");
}
invariant(temporalBuildNodeEntry !== undefined);
let temporalArgs = temporalBuildNodeEntry.args;
// replace the original props with the cloned one
let newTemporalArgs = temporalArgs.map(arg => (arg === props ? clonedProps : arg));
let preludeGenerator = realm.preludeGenerator;
invariant(preludeGenerator !== undefined);
let to = newTemporalArgs[0];
let temporalTo = AbstractValue.createTemporalFromBuildFunction(
realm,
ObjectValue,
newTemporalArgs,
([methodNode, targetNode, ...sourceNodes]: Array<BabelNodeExpression>) => {
return t.callExpression(methodNode, [targetNode, ...sourceNodes]);
([targetNode, ...sourceNodes]: Array<BabelNodeExpression>) => {
return t.callExpression(preludeGenerator.memoizeReference("Object.assign"), [targetNode, ...sourceNodes]);
},
{ skipInvariant: true }
{
skipInvariant: true,
mutatesOnly: [to],
temporalType: "OBJECT_ASSIGN",
}
);
invariant(temporalTo instanceof AbstractObjectValue);
invariant(clonedProps instanceof ObjectValue);
@ -1059,15 +1069,21 @@ export function applyObjectAssignConfigsForReactElement(realm: Realm, to: Object
// prepare our temporal Object.assign fallback
to.makePartial();
to.makeSimple();
let temporalArgs = [objAssign, to, ...delayedSources];
let temporalArgs = [to, ...delayedSources];
let preludeGenerator = realm.preludeGenerator;
invariant(preludeGenerator !== undefined);
let temporalTo = AbstractValue.createTemporalFromBuildFunction(
realm,
ObjectValue,
temporalArgs,
([methodNode, ..._args]) => {
return t.callExpression(methodNode, ((_args: any): Array<any>));
([..._args]) => {
return t.callExpression(preludeGenerator.memoizeReference("Object.assign"), ((_args: any): Array<any>));
},
{ skipInvariant: true, mutatesOnly: [to] }
{
skipInvariant: true,
mutatesOnly: [to],
temporalType: "OBJECT_ASSIGN",
}
);
invariant(temporalTo instanceof AbstractObjectValue);
temporalTo.values = new ValuesDomain(to);

View File

@ -64,7 +64,7 @@ import {
import type { Compatibility, RealmOptions, ReactOutputTypes, InvariantModeTypes } from "./options.js";
import invariant from "./invariant.js";
import seedrandom from "seedrandom";
import { Generator, PreludeGenerator, type TemporalBuildNodeEntryArgs } from "./utils/generator.js";
import { Generator, PreludeGenerator, type TemporalBuildNodeEntry } from "./utils/generator.js";
import { emptyExpression, voidExpression } from "./utils/babelhelpers.js";
import { Environment, Functions, Join, Properties, To, Widen, Path } from "./singletons.js";
import type { ReactSymbolTypes } from "./react/utils.js";
@ -248,6 +248,10 @@ export class Realm {
this.partialEvaluators = (Object.create(null): any);
this.$GlobalEnv = ((undefined: any): LexicalEnvironment);
this.derivedIds = new Map();
this.temporalEntryArgToEntries = new Map();
this.temporalEntryCounter = 0;
this.instantRender = {
enabled: opts.instantRender || false,
};
@ -341,6 +345,10 @@ export class Realm {
$GlobalEnv: LexicalEnvironment;
intrinsics: Intrinsics;
derivedIds: Map<string, TemporalBuildNodeEntry>;
temporalEntryArgToEntries: Map<Value, Set<TemporalBuildNodeEntry>>;
temporalEntryCounter: number;
instantRender: {
enabled: boolean,
};
@ -1807,12 +1815,29 @@ export class Realm {
return !this._abstractValuesDefined.has(nameString);
}
getTemporalBuildNodeEntryArgsFromDerivedValue(value: Value): void | TemporalBuildNodeEntryArgs {
getTemporalBuildNodeEntryFromDerivedValue(value: Value): void | TemporalBuildNodeEntry {
let name = value.intrinsicName;
invariant(name);
let preludeGenerator = this.preludeGenerator;
invariant(preludeGenerator !== undefined);
let temporalBuildNodeEntryArgs = preludeGenerator.derivedIds.get(name);
return temporalBuildNodeEntryArgs;
if (!name) {
return undefined;
}
let temporalBuildNodeEntry = value.$Realm.derivedIds.get(name);
return temporalBuildNodeEntry;
}
getTemporalGeneratorEntriesReferencingArg(arg: AbstractValue | ObjectValue): void | Set<TemporalBuildNodeEntry> {
return this.temporalEntryArgToEntries.get(arg);
}
saveTemporalGeneratorEntryArgs(temporalBuildNodeEntry: TemporalBuildNodeEntry): void {
let args = temporalBuildNodeEntry.args;
for (let arg of args) {
let temporalEntries = this.temporalEntryArgToEntries.get(arg);
if (temporalEntries === undefined) {
temporalEntries = new Set();
this.temporalEntryArgToEntries.set(arg, temporalEntries);
}
temporalEntries.add(temporalBuildNodeEntry);
}
}
}

View File

@ -21,7 +21,7 @@ import {
} from "../values/index.js";
import type { BabelNodeStatement } from "babel-types";
import type { SerializedBody } from "./types.js";
import { Generator, type TemporalBuildNodeEntryArgs } from "../utils/generator.js";
import { Generator, type TemporalBuildNodeEntry } from "../utils/generator.js";
import invariant from "../invariant.js";
import { BodyReference } from "./types.js";
import { ResidualFunctions } from "./ResidualFunctions.js";
@ -67,7 +67,7 @@ export class Emitter {
residualFunctions: ResidualFunctions,
referencedDeclaredValues: Map<Value, void | FunctionValue>,
conditionalFeasibility: Map<AbstractValue, { t: boolean, f: boolean }>,
derivedIds: Map<string, TemporalBuildNodeEntryArgs>
derivedIds: Map<string, TemporalBuildNodeEntry>
) {
this._mainBody = { type: "MainGenerator", parentBody: undefined, entries: [], done: false };
this._waitingForValues = new Map();

View File

@ -195,7 +195,7 @@ export class ResidualHeapSerializer {
this.residualFunctions,
referencedDeclaredValues,
conditionalFeasibility,
this.preludeGenerator.derivedIds
this.realm.derivedIds
);
this.mainBody = this.emitter.getBody();
this.residualHeapInspector = residualHeapInspector;
@ -1897,7 +1897,7 @@ export class ResidualHeapSerializer {
if (serializedValue.type === "Identifier") {
let id = ((serializedValue: any): BabelNodeIdentifier);
invariant(
!this.preludeGenerator.derivedIds.has(id.name) ||
!this.realm.derivedIds.has(id.name) ||
this.emitter.cannotDeclare() ||
this.emitter.hasBeenDeclared(val) ||
(this.emitter.emittingToAdditionalFunction() && this.referencedDeclaredValues.get(val) === undefined),
@ -2284,7 +2284,7 @@ export class ResidualHeapSerializer {
this.generator.serialize(this._getContext());
this.getStatistics().generators++;
invariant(this.emitter.declaredCount() <= this.preludeGenerator.derivedIds.size);
invariant(this.emitter.declaredCount() <= this.realm.derivedIds.size);
// TODO #20: add timers

View File

@ -10,7 +10,7 @@
/* @flow strict-local */
import { DeclarativeEnvironmentRecord, type Binding } from "../environment.js";
import { ConcreteValue, Value, ObjectValue, AbstractValue } from "../values/index.js";
import { AbstractValue, ConcreteValue, ObjectValue, Value } from "../values/index.js";
import type { ECMAScriptSourceFunctionValue, FunctionValue } from "../values/index.js";
import type { BabelNodeExpression, BabelNodeStatement, BabelNodeMemberExpression } from "babel-types";
import { SameValue } from "../methods/abstract.js";

View File

@ -89,6 +89,8 @@ export type VisitEntryCallbacks = {|
visitBindingAssignment: (Binding, Value) => Value,
|};
export type TemporalBuildNodeType = "OBJECT_ASSIGN";
export type DerivedExpressionBuildNodeFunction = (
Array<BabelNodeExpression>,
SerializationContext,
@ -102,6 +104,16 @@ export type GeneratorBuildNodeFunction = (
) => BabelNodeStatement;
export class GeneratorEntry {
constructor(realm: Realm) {
// We increment the index of every TemporalBuildNodeEntry created.
// This should match up as a form of timeline value due to the tree-like
// structure we use to create entries during evaluation. For example,
// if all AST nodes in a BlockStatement resulted in a temporal build node
// for each AST node, then each would have a sequential index as to its
// position of how it was evaluated in the BlockSstatement.
this.index = realm.temporalEntryCounter++;
}
visit(callbacks: VisitEntryCallbacks, containingGenerator: Generator): boolean {
invariant(false, "GeneratorEntry is an abstract base class");
}
@ -113,6 +125,16 @@ export class GeneratorEntry {
getDependencies(): void | Array<Generator> {
invariant(false, "GeneratorEntry is an abstract base class");
}
notEqualToAndDoesNotHappenBefore(entry: GeneratorEntry): boolean {
return this.index > entry.index;
}
notEqualToAndDoesNotHappenAfter(entry: GeneratorEntry): boolean {
return this.index < entry.index;
}
index: number;
}
export type TemporalBuildNodeEntryArgs = {
@ -123,11 +145,12 @@ export type TemporalBuildNodeEntryArgs = {
dependencies?: Array<Generator>,
isPure?: boolean,
mutatesOnly?: Array<Value>,
temporalType?: TemporalBuildNodeType,
};
export class TemporalBuildNodeEntry extends GeneratorEntry {
constructor(args: TemporalBuildNodeEntryArgs) {
super();
constructor(realm: Realm, args: TemporalBuildNodeEntryArgs) {
super(realm);
Object.assign(this, args);
if (this.mutatesOnly !== undefined) {
invariant(!this.isPure);
@ -144,6 +167,7 @@ export class TemporalBuildNodeEntry extends GeneratorEntry {
dependencies: void | Array<Generator>;
isPure: void | boolean;
mutatesOnly: void | Array<Value>;
temporalType: void | TemporalBuildNodeType;
visit(callbacks: VisitEntryCallbacks, containingGenerator: Generator): boolean {
let omit = this.isPure && this.declared && callbacks.canOmit(this.declared);
@ -210,6 +234,38 @@ export class TemporalBuildNodeEntry extends GeneratorEntry {
}
}
export class TemporalObjectAssignEntry extends TemporalBuildNodeEntry {
visit(callbacks: VisitEntryCallbacks, containingGenerator: Generator): boolean {
let declared = this.declared;
if (!(declared instanceof AbstractObjectValue || declared instanceof ObjectValue)) {
return false;
}
let realm = declared.$Realm;
// The only optimization we attempt to do to Object.assign for now is merging of multiple entries
// into a new generator entry.
let result = attemptToMergeEquivalentObjectAssigns(realm, callbacks, this);
if (result instanceof TemporalObjectAssignEntry) {
let nextResult = result;
while (nextResult instanceof TemporalObjectAssignEntry) {
nextResult = attemptToMergeEquivalentObjectAssigns(realm, callbacks, result);
// If we get back a TemporalObjectAssignEntry, then we have successfully merged a single
// Object.assign, but we may be able to merge more. So repeat the process.
if (nextResult instanceof TemporalObjectAssignEntry) {
result = nextResult;
}
}
// We have an optimized temporal entry, so replace the current temporal
// entry and visit that entry instead.
this.args = result.args;
} else if (result === "POSSIBLE_OPTIMIZATION") {
callbacks.recordDelayedEntry(containingGenerator, this);
return false;
}
return super.visit(callbacks, containingGenerator);
}
}
type ModifiedPropertyEntryArgs = {|
propertyBinding: PropertyBinding,
newDescriptor: void | Descriptor,
@ -217,8 +273,8 @@ type ModifiedPropertyEntryArgs = {|
|};
class ModifiedPropertyEntry extends GeneratorEntry {
constructor(args: ModifiedPropertyEntryArgs) {
super();
constructor(realm: Realm, args: ModifiedPropertyEntryArgs) {
super(realm);
Object.assign(this, args);
}
@ -255,8 +311,8 @@ type ModifiedBindingEntryArgs = {|
|};
class ModifiedBindingEntry extends GeneratorEntry {
constructor(args: ModifiedBindingEntryArgs) {
super();
constructor(realm: Realm, args: ModifiedBindingEntryArgs) {
super(realm);
Object.assign(this, args);
}
@ -309,8 +365,8 @@ class ModifiedBindingEntry extends GeneratorEntry {
}
class ReturnValueEntry extends GeneratorEntry {
constructor(generator: Generator, returnValue: Value) {
super();
constructor(realm: Realm, generator: Generator, returnValue: Value) {
super(realm);
this.returnValue = returnValue.promoteEmptyToUndefined();
this.containingGenerator = generator;
}
@ -339,7 +395,7 @@ class ReturnValueEntry extends GeneratorEntry {
class IfThenElseEntry extends GeneratorEntry {
constructor(generator: Generator, completion: PossiblyNormalCompletion | ForkedAbruptCompletion, realm: Realm) {
super();
super(realm);
this.completion = completion;
this.containingGenerator = generator;
this.condition = completion.joinCondition;
@ -381,8 +437,8 @@ class IfThenElseEntry extends GeneratorEntry {
}
class BindingAssignmentEntry extends GeneratorEntry {
constructor(binding: Binding, value: Value) {
super();
constructor(realm: Realm, binding: Binding, value: Value) {
super(realm);
this.binding = binding;
this.value = value;
}
@ -518,7 +574,7 @@ export class Generator {
}
}
this._entries.push(
new ModifiedPropertyEntry({
new ModifiedPropertyEntry(this.realm, {
propertyBinding,
newDescriptor: desc,
containingGenerator: this,
@ -529,7 +585,7 @@ export class Generator {
emitBindingModification(modifiedBinding: Binding) {
invariant(this.effectsToApply !== undefined);
this._entries.push(
new ModifiedBindingEntry({
new ModifiedBindingEntry(this.realm, {
modifiedBinding,
newValue: modifiedBinding.value,
containingGenerator: this,
@ -538,7 +594,7 @@ export class Generator {
}
emitReturnValue(result: Value) {
this._entries.push(new ReturnValueEntry(this, result));
this._entries.push(new ReturnValueEntry(this.realm, this, result));
}
emitIfThenElse(result: PossiblyNormalCompletion | ForkedAbruptCompletion, realm: Realm) {
@ -587,7 +643,7 @@ export class Generator {
}
emitBindingAssignment(binding: Binding, value: Value) {
this._entries.push(new BindingAssignmentEntry(binding, value));
this._entries.push(new BindingAssignmentEntry(this.realm, binding, value));
}
emitPropertyAssignment(object: ObjectValue, key: string, value: Value) {
@ -957,6 +1013,7 @@ export class Generator {
isPure?: boolean,
skipInvariant?: boolean,
mutatesOnly?: Array<Value>,
temporalType?: TemporalBuildNodeType,
|}
): AbstractValue {
invariant(buildNode_ instanceof Function || args.length === 0);
@ -968,7 +1025,7 @@ export class Generator {
this.realm,
types,
values,
1735003607742176 + this.preludeGenerator.derivedIds.size,
1735003607742176 + this.realm.derivedIds.size,
[],
id,
options
@ -988,6 +1045,7 @@ export class Generator {
]);
},
mutatesOnly: optionalArgs ? optionalArgs.mutatesOnly : undefined,
temporalType: optionalArgs ? optionalArgs.temporalType : undefined,
});
let type = types.getType();
res.intrinsicName = id.name;
@ -1066,15 +1124,21 @@ export class Generator {
return res;
}
// PITFALL Warning: adding a new kind of TemporalBuildNodeEntry that is not the result of a join or composition
// will break this purgeEntriesWithGeneratorDepencies.
_addEntry(entry: TemporalBuildNodeEntryArgs): void {
this._entries.push(new TemporalBuildNodeEntry(entry));
_addEntry(entryArgs: TemporalBuildNodeEntryArgs): TemporalBuildNodeEntry {
let entry;
if (entryArgs.temporalType === "OBJECT_ASSIGN") {
entry = new TemporalObjectAssignEntry(this.realm, entryArgs);
} else {
entry = new TemporalBuildNodeEntry(this.realm, entryArgs);
}
this.realm.saveTemporalGeneratorEntryArgs(entry);
this._entries.push(entry);
return entry;
}
_addDerivedEntry(id: string, entry: TemporalBuildNodeEntryArgs): void {
this._addEntry(entry);
this.preludeGenerator.derivedIds.set(id, entry);
_addDerivedEntry(id: string, entryArgs: TemporalBuildNodeEntryArgs): void {
let entry = this._addEntry(entryArgs);
this.realm.derivedIds.set(id, entry);
}
appendGenerator(other: Generator, leadingComment: string): void {
@ -1174,7 +1238,6 @@ export class NameGenerator {
export class PreludeGenerator {
constructor(debugNames: ?boolean, uniqueSuffix: ?string) {
this.prelude = [];
this.derivedIds = new Map();
this.memoizedRefs = new Map();
this.nameGenerator = new NameGenerator(new Set(), !!debugNames, uniqueSuffix || "", "_$");
this.usesThis = false;
@ -1183,7 +1246,6 @@ export class PreludeGenerator {
}
prelude: Array<BabelNodeStatement>;
derivedIds: Map<string, TemporalBuildNodeEntryArgs>;
memoizedRefs: Map<string, BabelNodeIdentifier>;
nameGenerator: NameGenerator;
usesThis: boolean;
@ -1251,3 +1313,118 @@ export class PreludeGenerator {
return ref;
}
}
type TemporalBuildNodeEntryOptimizationStatus = "NO_OPTIMIZATION" | "POSSIBLE_OPTIMIZATION";
// This function attempts to optimize Object.assign calls, by merging mulitple
// calls into one another where possible. For example:
//
// var a = Object.assign({}, someAbstact);
// var b = Object.assign({}, a);
//
// Becomes:
// var b = Object.assign({}, someAbstract, a);
//
export function attemptToMergeEquivalentObjectAssigns(
realm: Realm,
callbacks: VisitEntryCallbacks,
temporalBuildNodeEntry: TemporalBuildNodeEntry
): TemporalBuildNodeEntryOptimizationStatus | TemporalObjectAssignEntry {
let args = temporalBuildNodeEntry.args;
// If we are Object.assigning 2 or more args
if (args.length < 2) {
return "NO_OPTIMIZATION";
}
let to = args[0];
// Then scan through the args after the "to" of this Object.assign, to see if any
// other sources are the "to" of a previous Object.assign call
loopThroughArgs: for (let i = 1; i < args.length; i++) {
let possibleOtherObjectAssignTo = args[i];
// Ensure that the "to" value can be omitted
// Note: this check is still somewhat fragile and depends on the visiting order
// but it's not a functional problem right now and can be better addressed at a
// later point.
if (!callbacks.canOmit(possibleOtherObjectAssignTo)) {
continue;
}
// Check if the "to" was definitely an Object.assign, it should
// be a snapshot AbstractObjectValue
if (possibleOtherObjectAssignTo instanceof AbstractObjectValue) {
let otherTemporalBuildNodeEntry = realm.getTemporalBuildNodeEntryFromDerivedValue(possibleOtherObjectAssignTo);
if (!(otherTemporalBuildNodeEntry instanceof TemporalObjectAssignEntry)) {
continue;
}
let otherArgs = otherTemporalBuildNodeEntry.args;
// Object.assign has at least 1 arg
if (otherArgs.length < 2) {
continue;
}
let otherArgsToUse = [];
for (let x = 1; x < otherArgs.length; x++) {
let arg = otherArgs[x];
// The arg might have been havoced, so ensure we do not continue in this case
if (arg instanceof ObjectValue && arg.mightBeHavocedObject()) {
continue loopThroughArgs;
}
if (arg instanceof ObjectValue || arg instanceof AbstractValue) {
let temporalGeneratorEntries = realm.getTemporalGeneratorEntriesReferencingArg(arg);
// We need to now check if there are any other temporal entries that exist
// between the Object.assign TemporalObjectAssignEntry that we're trying to
// merge and the current TemporalObjectAssignEntry we're going to merge into.
if (temporalGeneratorEntries !== undefined) {
for (let temporalGeneratorEntry of temporalGeneratorEntries) {
// If the entry is that of another Object.assign, then
// we know that this entry isn't going to cause issues
// with merging the TemporalObjectAssignEntry.
if (temporalGeneratorEntry instanceof TemporalObjectAssignEntry) {
continue;
}
// TODO: what if the temporalGeneratorEntry can be omitted and not needed?
// If the index of this entry exists between start and end indexes,
// then we cannot optimize and merge the TemporalObjectAssignEntry
// because another generator entry may have a dependency on the Object.assign
// TemporalObjectAssignEntry we're trying to merge.
if (
temporalGeneratorEntry.notEqualToAndDoesNotHappenBefore(otherTemporalBuildNodeEntry) &&
temporalGeneratorEntry.notEqualToAndDoesNotHappenAfter(temporalBuildNodeEntry)
) {
continue loopThroughArgs;
}
}
}
}
otherArgsToUse.push(arg);
}
// If we cannot omit the "to" value that means it's being used, so we shall not try to
// optimize this Object.assign.
if (!callbacks.canOmit(to)) {
let newArgs = [to, ...otherArgsToUse];
for (let x = 2; x < args.length; x++) {
let arg = args[x];
// We don't want to add the "to" that we're merging with!
if (arg !== possibleOtherObjectAssignTo) {
newArgs.push(arg);
}
}
// We now create a new TemporalObjectAssignEntry, without mutating the existing
// entry at this point. This new entry is essentially a TemporalObjectAssignEntry
// that contains two Object.assign call TemporalObjectAssignEntry entries that have
// been merged into a single entry. The previous Object.assign TemporalObjectAssignEntry
// should dead-code eliminate away once we replace the original TemporalObjectAssignEntry
// we started with with the new merged on as they will no longer be referenced.
let newTemporalObjectAssignEntryArgs = Object.assign({}, temporalBuildNodeEntry, {
args: newArgs,
});
return new TemporalObjectAssignEntry(realm, newTemporalObjectAssignEntryArgs);
}
// We might be able to optimize, but we are not sure because "to" can still omit.
// So we return possible optimization status and wait until "to" does get visited.
// It may never get visited, but that's okay as we'll skip the optimization all
// together.
return "POSSIBLE_OPTIMIZATION";
}
}
return "NO_OPTIMIZATION";
}

View File

@ -20,7 +20,7 @@ import type {
import { CompilerDiagnostic, FatalError } from "../errors.js";
import type { Realm } from "../realm.js";
import type { PropertyKeyValue } from "../types.js";
import { PreludeGenerator } from "../utils/generator.js";
import { PreludeGenerator, type TemporalBuildNodeType } from "../utils/generator.js";
import buildExpressionTemplate from "../utils/builder.js";
import {
@ -142,11 +142,10 @@ export default class AbstractValue extends Value {
addSourceNamesTo(names: Array<string>, visited: Set<AbstractValue> = new Set()) {
if (visited.has(this)) return;
visited.add(this);
let gen = this.$Realm.preludeGenerator;
let realm = this.$Realm;
function add_intrinsic(name: string) {
if (name.startsWith("_$")) {
if (gen === undefined) return;
let temporalBuildNodeEntryArgs = gen.derivedIds.get(name);
let temporalBuildNodeEntryArgs = realm.derivedIds.get(name);
invariant(temporalBuildNodeEntryArgs !== undefined);
add_args(temporalBuildNodeEntryArgs.args);
} else if (names.indexOf(name) < 0) {
@ -784,6 +783,7 @@ export default class AbstractValue extends Value {
isPure?: boolean,
skipInvariant?: boolean,
mutatesOnly?: Array<Value>,
temporalType?: TemporalBuildNodeType,
|}
): AbstractValue {
invariant(resultType !== UndefinedValue);
@ -824,6 +824,7 @@ export default class AbstractValue extends Value {
isPure?: boolean,
skipInvariant?: boolean,
mutatesOnly?: Array<Value>,
temporalType?: TemporalBuildNodeType,
|}
): AbstractValue | UndefinedValue {
let types = new TypesDomain(resultType);

View File

@ -1,4 +1,5 @@
// Copies of assign\(:0
global.f = function() {
var x = global.__abstract ? global.__abstract({}, "({})") : {};
global.__makeSimple && __makeSimple(x);

View File

@ -0,0 +1,16 @@
// Copies of _\$B:2
// inline expressions
// _$B is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo, bar) {
var a = Object.assign({}, foo, bar, {a: 1});
var b = Object.assign({}, a, {a: 2});
var c = Object.assign({}, b, {a: 2}, {d: 5});
return c;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({b: 1}, {c: 2})); }

View File

@ -0,0 +1,18 @@
// Copies of _\$B:2
// inline expressions
// _$B is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo, bar) {
var a = Object.assign({}, foo, bar, {a: 1});
foo = {};
var b = Object.assign({}, a, {a: 2});
bar = {};
var c = Object.assign({}, b, {a: 2}, {d: 5});
return c;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({b: 1}, {c: 2})); }

View File

@ -0,0 +1,22 @@
// Copies of _\$D:4
// inline expressions
// _$D is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(x, foo, bar) {
var a = Object.assign({}, foo, bar, {a: 1});
foo = {};
var b
if (x) {
b = Object.assign({}, a, {a: 2});
} else {
b = Object.assign({}, a, {a: 5});
}
var c = Object.assign({}, b, {a: 2}, {d: 5});
return c;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f(false, {b: 1}, {c: 2})); }

View File

@ -0,0 +1,15 @@
// Copies of _\$7:3
// inline expressions
// _$7 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(x, foo) {
var a = Object.assign({}, foo);
return x ? Object.assign({}, a, {a: 1}) : Object.assign({}, a, {a: 2})
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f(false, {a: 3})); }

View File

@ -0,0 +1,22 @@
// Copies of _\$4:3
// inline expressions
// _$4 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(x, foo) {
var a = Object.assign({}, foo);
var b = Object.assign({}, a);
// b gets visited
var someVal = b;
if (x) {
// a gets visited
someVal = a;
}
// a should still exist
return someVal;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f(false, {a: 3})); }

View File

@ -0,0 +1,25 @@
// Copies of _\$4:3
// inline expressions
// _$4 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(x, foo) {
var a = Object.assign({}, foo);
var b = Object.assign({}, a);
// b gets visited
var someVal = b;
if (x) {
// a gets visited
function foo() {
return a;
}
someVal = foo;
}
// a should still exist
return someVal;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f(false, {a: 3})); }

View File

@ -0,0 +1,16 @@
// Copies of _\$4:3
// inline expressions
// _$4 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o);
o.x = 42;
var q = Object.assign({}, p);
return q;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({x: 10})); }

View File

@ -0,0 +1,17 @@
// Copies of _\$C:2
// inline expressions
// _$C is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o, {a: 1});
var p2 = Object.assign({}, o, {a: 2});
var q = Object.assign({}, p, {a: 3});
var q2 = Object.assign({}, q, {a: 4});
return q2;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({a: 10})); }

View File

@ -0,0 +1,17 @@
// Copies of _\$C:3
// inline expressions
// _$C is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o, {a: 1});
var q = Object.assign({}, p, {a: 3});
var p2 = Object.assign({}, o, {a: 2});
var q2 = Object.assign({}, p2, {a: 4});
return [q, q2];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({a: 10})); }

View File

@ -0,0 +1,19 @@
// Copies of _\$H:3
// inline expressions
// _$H is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o, {a: 1});
var p2 = Object.assign({}, o, {a: 2});
p2.a = 100;
var q = Object.assign({}, p, {a: 3});
var q2 = Object.assign({}, q, {a: 4});
var q2 = Object.assign({}, q2, p2, {a: 1}, p2);
return q2;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({a: 10})); }

View File

@ -1,4 +1,5 @@
// Copies of .assign;:1
global.f = function() {
var x = global.__abstract ? global.__abstract({}, "({a: 1})") : {a: 1};
var val = {};

View File

@ -0,0 +1,18 @@
// Copies of _\$5:2
// inline expressions
// _$5 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o);
var l = {};
var q = Object.assign({}, p, l);
l.x = 42;
var r = Object.assign({}, q);
return r;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({x: 10})); }

View File

@ -0,0 +1,41 @@
// Copies of _\$E:4
// inline expressions
// _$E is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o, {a: 1});
var q = Object.assign({}, p, {a: 3});
var p2 = Object.assign({}, o, {a: 2});
var q2 = Object.assign({}, p2, {a: 4});
return [q, q2];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({a: 10})); }
function f(o1, o2, g, h) {
let a = Object.assign({}, o1);
let b = Object.assign({}, o2);
g(a, b);
let p = Object.assign({}, o1);
h(o2); // can mutate o1 !
let q = Object.assign({}, p);
return q;
}
if (global.__optimize) __optimize(f);
inspect = function() {
return f({}, {},
function(o1, o2) {
o2.o1 = o1;
},
function(o2) {
o2.o1.x = 42;
}
).x;
}

View File

@ -0,0 +1,40 @@
// Copies of _\$A:3
// inline expressions
// _$A is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(o) {
var p = Object.assign({}, o, {a: 1});
var q = Object.assign({}, p, {a: 3});
var p2 = Object.assign({}, o, {a: 2});
var q2 = Object.assign({}, p2, {a: 4});
return [q, q2];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({a: 10})); }
function f(o2, g, h) {
let o1 = {};
g(o1, o2); // leaks o1
let p = Object.assign({}, o1);
h(o2); // can mutate o1 !
let q = Object.assign({}, p);
return q;
}
if (global.__optimize) __optimize(f);
inspect = function() {
return f({},
function(o1, o2) {
o2.o1 = o1;
},
function(o2) {
o2.o1.x = 42;
}
).x;
}

View File

@ -1,4 +1,5 @@
// Copies of assign\(:0
global.f = function() {
var x = global.__abstract ? global.__abstract({}, "({})") : {};
global.__makeSimple && __makeSimple(x);

View File

@ -0,0 +1,17 @@
// Copies of _\$4:2
// inline expressions
// Why? _$4 is the variable for Object.assign, and there should be
// two copies of it. One for it's declaration and one for its reference.
// We use inline expressions on all test iterations to ensure the copies
// count is always constant.
function f(foo) {
var a = Object.assign({}, foo);
var b = Object.assign({}, a);
return b;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return f({}); }

View File

@ -0,0 +1,15 @@
// Copies of _\$4:3
// inline expressions
// _$4 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo) {
var a = Object.assign({}, foo);
var b = Object.assign({}, a);
return [a, b];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({})); }

View File

@ -0,0 +1,16 @@
// Copies of _\$5:3
// inline expressions
// _$5 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo) {
var a = Object.assign({}, foo);
var bar = a.x;
var b = Object.assign({}, a);
return [b, bar];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({x: 1})); }

View File

@ -0,0 +1,16 @@
// Copies of _\$5:2
// inline expressions
// _$5 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo) {
var a = Object.assign({}, foo);
var bar = a.x;
var b = Object.assign({}, a);
return [a, bar];
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({x: 1})); }

View File

@ -0,0 +1,15 @@
// Copies of _\$6:2
// inline expressions
// _$6 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo, bar) {
var a = Object.assign({}, foo, bar, {a: 1});
var b = Object.assign({}, a);
return b;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({b: 1}, {c: 2})); }

View File

@ -0,0 +1,15 @@
// Copies of _\$7:2
// inline expressions
// _$7 is the variable for Object.assign. See DeadObjectAssign4.js for
// a larger explanation.
function f(foo, bar) {
var a = Object.assign({}, foo, bar, {a: 1});
var b = Object.assign({}, a, {a: 2});
return b;
}
if (global.__optimize) __optimize(f);
global.inspect = function() { return JSON.stringify(f({b: 1}, {c: 2})); }