Leaked declarative bindings are effectively serialized as simple variables (#2125)

Summary:
Release notes: Fixes to leaked declarative bindings

Leaked declarative bindings need to be accessed out-of-band as far
as the serializer is concerned which otherwise only puts final
values into locations.

To make that work, whenever two states are merged where a
declarative binding in one has been leaked but not in the other,
this now gets harmonized by also recording the materialization
side effect into the generator of the other.

The result is that a binding always ends up being fully leaked or not.
For leaked mutable bindings, the tracked value becomes irrelevant, and
is now truly made unavailable. For leaked immutable bindings, the last
value is retained, and we no longer need the strange `leakedImmutableValue` field.

Leaked bindings are now referentialized as simple variables, bypassing the
delayed captured-scope logic. This produces nicer more efficient code, but,
at least for now, has the downside that it rules out sharing of function bodies.

This change is a step towards the (to be publicly documented)
long-term plan to clean up Prepack's handling of leaking and havocing.
(Similar to what this change does for declarative bindings, we'll also must
revisit what happens for leaked object properties. But that's for later.)

This fixes #2122 (issue havocing value in conditional) and adds regression test.
This fixes #2007 (Conceptual issue with havoced bindings) and adds regression test.
Closes https://github.com/facebook/prepack/pull/2125

Differential Revision: D8549299

Pulled By: NTillmann

fbshipit-source-id: 28c3681beafd3890668b72a828f59582c636a6c9
This commit is contained in:
Nikolai Tillmann 2018-06-20 14:25:09 -07:00 committed by Facebook Github Bot
parent 0cdafba476
commit d781c5b8ea
15 changed files with 207 additions and 66 deletions

View File

@ -66,9 +66,13 @@ export function havocBinding(binding: Binding) {
realm.recordModifiedBinding(binding).hasLeaked = true;
if (value !== undefined) {
let realmGenerator = realm.generator;
if (realmGenerator !== undefined) realmGenerator.emitBindingAssignment(binding, value);
if (binding.mutable !== true) binding.leakedImmutableValue = value;
binding.value = realm.intrinsics.undefined;
if (realmGenerator !== undefined && value !== realm.intrinsics.undefined)
realmGenerator.emitBindingAssignment(binding, value);
if (binding.mutable === true) {
// For mutable, i.e. non-const bindings, the actual value is no longer directly available.
// Thus, we reset the value to undefined to prevent any use of the last known value.
binding.value = undefined;
}
}
}
}
@ -154,7 +158,6 @@ export type Binding = {
// bindings that are assigned to inside loops with abstract termination conditions need temporal locations
phiNode?: AbstractValue,
hasLeaked: boolean,
leakedImmutableValue?: Value,
};
// ECMA262 8.1.1.1
@ -330,8 +333,8 @@ export class DeclarativeEnvironmentRecord extends EnvironmentRecord {
}
// 4. Return the value currently bound to N in envRec.
if (binding.hasLeaked) {
return binding.leakedImmutableValue || deriveGetBinding(realm, binding);
if (binding.hasLeaked && binding.mutable) {
return deriveGetBinding(realm, binding);
}
invariant(binding.value);
return binding.value;

View File

@ -451,7 +451,7 @@ export function OrdinaryCallEvaluateBody(
realm.savedCompletion = undefined;
let c = getCompletion();
// We are about the leave this function and this presents a join point where all non exeptional control flows
// We are about the leave this function and this presents a join point where all non exceptional control flows
// converge into a single flow using their joint effects to update the post join point state.
if (!(c instanceof ReturnCompletion)) {
if (!(c instanceof AbruptCompletion)) {

View File

@ -489,7 +489,7 @@ export class JoinImplementation {
generator: generator1,
modifiedBindings: modifiedBindings1,
modifiedProperties: modifiedProperties1,
createdObjects: createdObject1,
createdObjects: createdObjects1,
} = e1;
let {
@ -508,27 +508,34 @@ export class JoinImplementation {
}
} else if (result2 instanceof AbruptCompletion) {
invariant(result instanceof PossiblyNormalCompletion);
return new Effects(result, generator1, modifiedBindings1, modifiedProperties1, createdObject1);
return new Effects(result, generator1, modifiedBindings1, modifiedProperties1, createdObjects1);
}
let bindings = this.joinBindings(realm, joinCondition, modifiedBindings1, modifiedBindings2);
let [modifiedGenerator1, modifiedGenerator2, bindings] = this.joinBindings(
realm,
joinCondition,
generator1,
modifiedBindings1,
generator2,
modifiedBindings2
);
let properties = this.joinPropertyBindings(
realm,
joinCondition,
modifiedProperties1,
modifiedProperties2,
createdObject1,
createdObjects1,
createdObjects2
);
let createdObjects = new Set();
createdObject1.forEach(o => {
createdObjects1.forEach(o => {
createdObjects.add(o);
});
createdObjects2.forEach(o => {
createdObjects.add(o);
});
let generator = joinGenerators(realm, joinCondition, generator1, generator2);
let generator = joinGenerators(realm, joinCondition, modifiedGenerator1, modifiedGenerator2);
return new Effects(result, generator, bindings, properties, createdObjects);
}
@ -706,42 +713,61 @@ export class JoinImplementation {
// sets of m1 and m2. The value of a pair is the join of m1[key] and m2[key]
// where the join is defined to be just m1[key] if m1[key] === m2[key] and
// and abstract value with expression "joinCondition ? m1[key] : m2[key]" if not.
joinBindings(realm: Realm, joinCondition: AbstractValue, m1: Bindings, m2: Bindings): Bindings {
joinBindings(
realm: Realm,
joinCondition: AbstractValue,
g1: Generator,
m1: Bindings,
g2: Generator,
m2: Bindings
): [Generator, Generator, Bindings] {
let getAbstractValue = (v1: void | Value, v2: void | Value) => {
return AbstractValue.createFromConditionalOp(realm, joinCondition, v1, v2);
};
let rewritten1 = false;
let rewritten2 = false;
let leak = (b: Binding, g: Generator, v: void | Value, rewritten: boolean) => {
// just like to what happens in havocBinding, we are going to append a
// binding-assignment generator entry; however, we play it safe and don't
// mutate the generator; instead, we create a new one that wraps around the old one.
if (!rewritten) {
let h = new Generator(realm, "RewrittenToAppendBindingAssignments");
if (!g.empty()) h.appendGenerator(g, "");
g = h;
rewritten = true;
}
if (v !== undefined && v !== realm.intrinsics.undefined) g.emitBindingAssignment(b, v);
return [g, rewritten];
};
let join = (b: Binding, b1: void | BindingEntry, b2: void | BindingEntry) => {
let l1 = b1 === undefined ? b.hasLeaked : b1.hasLeaked;
let l2 = b2 === undefined ? b.hasLeaked : b2.hasLeaked;
let v1 = b1 === undefined ? b.value : b1.value;
let v2 = b2 === undefined ? b.value : b2.value;
let liv1 = b1 === undefined ? b.leakedImmutableValue : b1.leakedImmutableValue;
let liv2 = b2 === undefined ? b.leakedImmutableValue : b2.leakedImmutableValue;
let hasLeaked = l1 || l2; // If either has leaked, then this binding has leaked.
let value = this.joinValues(realm, v1, v2, getAbstractValue);
let leakedImmutableValue =
liv1 === undefined && liv2 === undefined ? undefined : this.joinValues(realm, liv1, liv2, getAbstractValue);
invariant(value instanceof Value);
invariant(leakedImmutableValue === undefined || leakedImmutableValue instanceof Value);
let previousLeakedImmutableValue, previousHasLeaked, previousValue;
// ensure that if either none or both sides have leaked
// note that if one side didn't have a binding entry yet, then there's nothing to actively leak
if (!l1 && l2) [g1, rewritten1] = leak(b, g1, v1, rewritten1);
else if (l1 && !l2) [g2, rewritten2] = leak(b, g2, v2, rewritten2);
let hasLeaked = l1 || l2;
// For leaked (and mutable) bindings, the actual value is no longer directly available.
// In that case, we reset the value to undefined to prevent any use of the last known value.
let value = hasLeaked ? undefined : this.joinValues(realm, v1, v2, getAbstractValue);
invariant(value === undefined || value instanceof Value);
let previousHasLeaked, previousValue;
if (b1 !== undefined) {
previousLeakedImmutableValue = b1.previousLeakedImmutableValue;
previousHasLeaked = b1.previousHasLeaked;
previousValue = b1.previousValue;
invariant(
b2 === undefined ||
(previousLeakedImmutableValue === b2.previousLeakedImmutableValue &&
previousHasLeaked === b2.previousHasLeaked &&
previousValue === b2.previousValue)
b2 === undefined || (previousHasLeaked === b2.previousHasLeaked && previousValue === b2.previousValue)
);
} else if (b2 !== undefined) {
previousLeakedImmutableValue = b2.previousLeakedImmutableValue;
previousHasLeaked = b2.previousHasLeaked;
previousValue = b2.previousValue;
}
return { leakedImmutableValue, hasLeaked, value, previousLeakedImmutableValue, previousHasLeaked, previousValue };
return { hasLeaked, value, previousHasLeaked, previousValue };
};
return this.joinMaps(m1, m2, join);
let joinedBindings = this.joinMaps(m1, m2, join);
return [g1, g2, joinedBindings];
}
// If v1 is known and defined and v1 === v2 return v1,

View File

@ -169,14 +169,11 @@ export class WidenImplementation {
result._buildNode = args => t.identifier(phiName);
}
invariant(result instanceof Value);
let previousLeakedImmutableValue = b2.previousLeakedImmutableValue;
let previousHasLeaked = b2.previousHasLeaked;
let previousValue = b2.previousValue;
return {
leakedImmutableValue: previousLeakedImmutableValue,
hasLeaked: previousHasLeaked,
value: result,
previousLeakedImmutableValue,
previousHasLeaked,
previousValue,
};
@ -378,8 +375,7 @@ export class WidenImplementation {
b1.value === undefined ||
b2.value === undefined ||
!this._containsValues(b1.value, b2.value) ||
b1.hasLeaked !== b2.hasLeaked ||
b1.leakedImmutableValue !== b2.leakedImmutableValue
b1.hasLeaked !== b2.hasLeaked
) {
return false;
}

View File

@ -71,10 +71,8 @@ import type { BabelNode, BabelNodeSourceLocation, BabelNodeLVal, BabelNodeStatem
import * as t from "babel-types";
export type BindingEntry = {
leakedImmutableValue: void | Value,
hasLeaked: void | boolean,
value: void | Value,
previousLeakedImmutableValue: void | Value,
previousHasLeaked: void | boolean,
previousValue: void | Value,
};
@ -1363,10 +1361,8 @@ export class Realm {
if (this.modifiedBindings !== undefined && !this.modifiedBindings.has(binding)) {
this.modifiedBindings.set(binding, {
leakedImmutableValue: undefined,
hasLeaked: undefined,
value: undefined,
previousLeakedImmutableValue: binding.leakedImmutableValue,
previousHasLeaked: binding.hasLeaked,
previousValue: binding.value,
});
@ -1440,8 +1436,7 @@ export class Realm {
redoBindings(modifiedBindings: void | Bindings) {
if (modifiedBindings === undefined) return;
modifiedBindings.forEach(({ leakedImmutableValue, hasLeaked, value }, binding, m) => {
binding.leakedImmutableValue = leakedImmutableValue;
modifiedBindings.forEach(({ hasLeaked, value }, binding, m) => {
binding.hasLeaked = hasLeaked || false;
binding.value = value;
});
@ -1450,10 +1445,8 @@ export class Realm {
undoBindings(modifiedBindings: void | Bindings) {
if (modifiedBindings === undefined) return;
modifiedBindings.forEach((entry, binding, m) => {
if (entry.leakedImmutableValue === undefined) entry.leakedImmutableValue = binding.leakedImmutableValue;
if (entry.hasLeaked === undefined) entry.hasLeaked = binding.hasLeaked;
if (entry.value === undefined) entry.value = binding.value;
binding.leakedImmutableValue = entry.previousLeakedImmutableValue;
binding.hasLeaked = entry.previousHasLeaked || false;
binding.value = entry.previousValue;
});

View File

@ -41,13 +41,15 @@ export class Referentializer {
realm: Realm,
options: SerializerOptions,
scopeNameGenerator: NameGenerator,
scopeBindingNameGenerator: NameGenerator
scopeBindingNameGenerator: NameGenerator,
leakedNameGenerator: NameGenerator
) {
this._options = options;
this.scopeNameGenerator = scopeNameGenerator;
this.scopeBindingNameGenerator = scopeBindingNameGenerator;
this.referentializationState = new Map();
this._leakedNameGenerator = leakedNameGenerator;
this.realm = realm;
}
@ -58,6 +60,7 @@ export class Referentializer {
_newCapturedScopeInstanceIdx: number;
referentializationState: Map<ReferentializationScope, ReferentializationState>;
_leakedNameGenerator: NameGenerator;
getStatistics(): SerializerStatistics {
invariant(this.realm.statistics instanceof SerializerStatistics, "serialization requires SerializerStatistics");
@ -81,15 +84,23 @@ export class Referentializer {
);
}
createLeakedIds(referentializationScope: ReferentializationScope): Array<BabelNodeStatement> {
const leakedIds = [];
const serializedScopes = this._getReferentializationState(referentializationScope).serializedScopes;
for (const scopeBinding of serializedScopes.values()) leakedIds.push(...scopeBinding.leakedIds);
if (leakedIds.length === 0) return [];
return [t.variableDeclaration("var", leakedIds.map(id => t.variableDeclarator(id)))];
}
createCapturedScopesPrelude(referentializationScope: ReferentializationScope): Array<BabelNodeStatement> {
let accessFunctionDeclaration = this._createCaptureScopeAccessFunction(referentializationScope);
if (accessFunctionDeclaration === undefined) return [];
return [accessFunctionDeclaration, this._createCapturedScopesArrayInitialization(referentializationScope)];
}
// Generate a shared function for accessing captured scope bindings.
// TODO: skip generating this function if the captured scope is not shared by multiple residual functions.
createCaptureScopeAccessFunction(referentializationScope: ReferentializationScope): BabelNodeStatement {
const body = [];
const selectorParam = t.identifier("__selector");
const captured = t.identifier("__captured");
const capturedScopesArray = this._getReferentializationState(referentializationScope).capturedScopesArray;
const selectorExpression = t.memberExpression(capturedScopesArray, selectorParam, /*Indexer syntax*/ true);
_createCaptureScopeAccessFunction(referentializationScope: ReferentializationScope): void | BabelNodeStatement {
// One switch case for one scope.
const cases = [];
const serializedScopes = this._getReferentializationState(referentializationScope).serializedScopes;
@ -99,6 +110,7 @@ export class Referentializer {
|};
const initializationCases: Map<string, InitializationCase> = new Map();
for (const scopeBinding of serializedScopes.values()) {
if (scopeBinding.initializationValues.length === 0) continue;
const expr = t.arrayExpression((scopeBinding.initializationValues: any));
const key = generate(expr, {}, "").code;
if (!initializationCases.has(key)) {
@ -112,6 +124,13 @@ export class Referentializer {
ic.scopeIDs.push(scopeBinding.id);
}
}
if (initializationCases.size === 0) return undefined;
const body = [];
const selectorParam = t.identifier("__selector");
const captured = t.identifier("__captured");
const capturedScopesArray = this._getReferentializationState(referentializationScope).capturedScopesArray;
const selectorExpression = t.memberExpression(capturedScopesArray, selectorParam, /*Indexer syntax*/ true);
for (const ic of initializationCases.values()) {
ic.scopeIDs.forEach((id, i) => {
let consequent: Array<BabelNodeStatement> = [];
@ -163,6 +182,7 @@ export class Referentializer {
name: this.scopeNameGenerator.generate(),
id: refState.capturedScopeInstanceIdx++,
initializationValues: [],
leakedIds: [],
referentializationScope,
};
refState.serializedScopes.set(declarativeEnvironmentRecord, scope);
@ -193,7 +213,20 @@ export class Referentializer {
return [t.variableDeclaration("var", [t.variableDeclarator(t.identifier(capturedScope), init)])];
}
referentializeBinding(residualBinding: ResidualFunctionBinding): void {
referentializeLeakedBinding(residualBinding: ResidualFunctionBinding): void {
invariant(residualBinding.hasLeaked);
// When simpleClosures is enabled, then space for captured mutable bindings is allocated upfront.
let serializedBindingId = t.identifier(this._leakedNameGenerator.generate(residualBinding.name));
let scope = this._getSerializedBindingScopeInstance(residualBinding);
scope.leakedIds.push(serializedBindingId);
residualBinding.serializedValue = residualBinding.serializedUnscopedLocation = serializedBindingId;
this.getStatistics().referentialized++;
}
referentializeModifiedBinding(residualBinding: ResidualFunctionBinding): void {
invariant(residualBinding.modified);
// Space for captured mutable bindings is allocated lazily.
let scope = this._getSerializedBindingScopeInstance(residualBinding);
let capturedScope = "__captured" + scope.name;
@ -258,6 +291,7 @@ export class Referentializer {
let scope = refState.serializedScopes.get(declarativeEnvironmentRecord);
if (scope) {
scope.initializationValues = [];
scope.leakedIds = [];
}
}
}
@ -274,7 +308,7 @@ export class Referentializer {
// Initialize captured scope at function call instead of globally
if (!residualBinding.declarativeEnvironmentRecord) residualBinding.referentialized = true;
if (!residualBinding.referentialized) {
this._getSerializedBindingScopeInstance(residualBinding);
if (!residualBinding.hasLeaked) this._getSerializedBindingScopeInstance(residualBinding);
residualBinding.referentialized = true;
}
@ -286,7 +320,7 @@ export class Referentializer {
}
}
createCapturedScopesArrayInitialization(referentializationScope: ReferentializationScope): BabelNodeStatement {
_createCapturedScopesArrayInitialization(referentializationScope: ReferentializationScope): BabelNodeStatement {
return t.variableDeclaration("var", [
t.variableDeclarator(
this._getReferentializationState(referentializationScope).capturedScopesArray,

View File

@ -147,7 +147,10 @@ export class ResidualFunctions {
let functionInfo = this.residualFunctionInfos.get(funcBody);
invariant(functionInfo);
let { usesArguments } = functionInfo;
return !shouldInlineFunction() && instances.length > 1 && !usesArguments;
let hasAnyLeakedIds = false;
for (const instance of instances)
for (const scope of instance.scopeInstances.values()) if (scope.leakedIds.length > 0) hasAnyLeakedIds = true;
return !shouldInlineFunction() && instances.length > 1 && !usesArguments && !hasAnyLeakedIds;
}
_getIdentifierReplacements(
@ -617,8 +620,10 @@ export class ResidualFunctions {
invariant(serializedValue);
return serializedValue;
});
for (let entry of instance.scopeInstances) {
flatArgs.push(t.numericLiteral(entry[1].id));
let hasAnyLeakedIds = false;
for (const scope of instance.scopeInstances.values()) {
flatArgs.push(t.numericLiteral(scope.id));
if (scope.leakedIds.length > 0) hasAnyLeakedIds = true;
}
let funcNode;
let firstUsage = this.firstFunctionUsages.get(functionValue);
@ -630,7 +635,8 @@ export class ResidualFunctions {
usesThis ||
hasFunctionArg ||
(firstUsage !== undefined && !firstUsage.isNotEarlierThan(insertionPoint)) ||
this.functionPrototypes.get(functionValue) !== undefined
this.functionPrototypes.get(functionValue) !== undefined ||
hasAnyLeakedIds
) {
let callArgs: Array<BabelNodeExpression | BabelNodeSpreadElement> = [t.thisExpression()];
for (let flatArg of flatArgs) callArgs.push(flatArg);
@ -673,8 +679,10 @@ export class ResidualFunctions {
} else {
prelude = this.prelude;
}
prelude.unshift(this.referentializer.createCaptureScopeAccessFunction(referentializationScope));
prelude.unshift(this.referentializer.createCapturedScopesArrayInitialization(referentializationScope));
prelude.unshift(
...this.referentializer.createCapturedScopesPrelude(referentializationScope),
...this.referentializer.createLeakedIds(referentializationScope)
);
}
for (let instance of this.functionInstances.reverse()) {

View File

@ -673,10 +673,15 @@ export class ResidualHeapSerializer {
let value = residualFunctionBinding.value;
invariant(residualFunctionBinding.declarativeEnvironmentRecord);
residualFunctionBinding.serializedValue = value !== undefined ? this.serializeValue(value) : voidExpression;
if (residualFunctionBinding.modified) {
this.referentializer.referentializeBinding(residualFunctionBinding);
if (residualFunctionBinding.hasLeaked) {
this.referentializer.referentializeLeakedBinding(residualFunctionBinding);
} else {
residualFunctionBinding.serializedValue = value !== undefined ? this.serializeValue(value) : voidExpression;
if (residualFunctionBinding.modified) {
this.referentializer.referentializeModifiedBinding(residualFunctionBinding);
}
}
if (value !== undefined && value.mightBeObject()) {
// Increment ref count one more time to ensure that this object will be assigned a unique id.
// This ensures that only once instance is created across all possible residual function invocations.

View File

@ -602,6 +602,7 @@ export class ResidualHeapVisitor {
}
_visitBindingHelper(residualFunctionBinding: ResidualFunctionBinding) {
if (residualFunctionBinding.hasLeaked) return;
let environment = residualFunctionBinding.declarativeEnvironmentRecord;
invariant(environment !== null);
if (residualFunctionBinding.value === undefined) {
@ -690,6 +691,7 @@ export class ResidualHeapVisitor {
name,
value: undefined,
modified: true,
hasLeaked: false,
declarativeEnvironmentRecord: null,
potentialReferentializationScopes: new Set(),
};
@ -717,6 +719,7 @@ export class ResidualHeapVisitor {
name,
value: undefined,
modified: false,
hasLeaked: false,
declarativeEnvironmentRecord: environment,
potentialReferentializationScopes: new Set(),
};
@ -1153,6 +1156,7 @@ export class ResidualHeapVisitor {
visitBindingAssignment: (binding: Binding, value: Value) => {
let residualBinding = this.getBinding(binding.environment, binding.name);
residualBinding.modified = true;
residualBinding.hasLeaked = true;
return this.visitEquivalentValue(value);
},
};

View File

@ -141,7 +141,8 @@ export class Serializer {
this.realm,
this.options,
preludeGenerator.createNameGenerator("__scope_"),
preludeGenerator.createNameGenerator("__get_scope_binding_")
preludeGenerator.createNameGenerator("__get_scope_binding_"),
preludeGenerator.createNameGenerator("__leaked_")
);
if (this.realm.react.verbose) {
this.logger.logInformation(`Visiting evaluated nodes...`);

View File

@ -107,6 +107,7 @@ export type ResidualFunctionBinding = {
name: string,
value: void | Value,
modified: boolean,
hasLeaked: boolean,
// null means a global binding
declarativeEnvironmentRecord: null | DeclarativeEnvironmentRecord,
// The serializedValue is only not yet present during the initialization of a binding that involves recursive dependencies.
@ -127,6 +128,7 @@ export type ScopeBinding = {
name: string,
id: number,
initializationValues: Array<BabelNodeExpression>,
leakedIds: Array<BabelNodeIdentifier>,
capturedScope?: string,
referentializationScope: ReferentializationScope,
};

View File

@ -792,7 +792,14 @@ export type JoinType = {
// sets of m1 and m2. The value of a pair is the join of m1[key] and m2[key]
// where the join is defined to be just m1[key] if m1[key] === m2[key] and
// and abstract value with expression "joinCondition ? m1[key] : m2[key]" if not.
joinBindings(realm: Realm, joinCondition: AbstractValue, m1: Bindings, m2: Bindings): Bindings,
joinBindings(
realm: Realm,
joinCondition: AbstractValue,
g1: Generator,
m1: Bindings,
g2: Generator,
m2: Bindings
): [Generator, Generator, Bindings],
// If v1 is known and defined and v1 === v2 return v1,
// otherwise return getAbstractValue(v1, v2)

View File

@ -0,0 +1,27 @@
// This test is there to check for a regression where code was generated
// that used a variable before it was declared, which trips the linter.
// The issue arose when joining a binding from a declarative environment record that only existed in one branch of the joined executions.
(function() {
function makeClosure(bar) {
if (bar) return null;
var captured = bar;
return function closure() {
return captured;
}
}
function fn(arg) {
if (arg) return undefined;
var state = {};
state.closure = makeClosure(arg.bar);
arg.baz(state);
}
global.fn = fn;
if (global.__optimize) {
__optimize(fn);
}
inspect = function() { return fn(true); }
})();

View File

@ -0,0 +1,16 @@
function fn(x, y, abstractVal) {
var value = x.toString();
if (y) {
abstractVal(function() {
value += "-next";
});
}
return value;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(10, false, function() {});
};

View File

@ -0,0 +1,19 @@
(function () {
function f(c, g) {
let x = 23;
let y;
if (c) {
x = Date.now();
function h() { y = x; x++; }
g(h);
return x - y;
} else {
x = Date.now();
function h() { y = x; x++; }
g(h);
return x - y;
}
}
global.__optimize && __optimize(f);
global.inspect = function() { return f(true, g => g()); }
})();