Handle computed property names using loop variables

Summary:
Initial bit of support for dealing with abstract loops where the loop body contains assignments to properties with computed names involving loop controlled variables.
Closes https://github.com/facebook/prepack/pull/1286

Differential Revision: D6628885

Pulled By: hermanventer

fbshipit-source-id: 960fa469e3c18e55e5447ac441235a80df2b1629
This commit is contained in:
Herman Venter 2017-12-22 13:12:35 -08:00 committed by Facebook Github Bot
parent 1bdf429281
commit 83508ef1ee
11 changed files with 144 additions and 58 deletions

View File

@ -43,7 +43,7 @@
"test-node-cli-mode": "bash < scripts/test-node-cli-mode.sh",
"test-std-in": "bash < scripts/test-std-in.sh",
"test-react": "jest scripts/test-react",
"test": "yarn test-residual && yarn test-serializer && yarn test-sourcemaps && yarn test-error-handler && yarn test-std-in && yarn test-test262 && yarn test-react && yarn test-internal",
"test": "yarn test-residual && yarn test-serializer && yarn test-sourcemaps && yarn test-error-handler && yarn test-std-in && yarn test-test262 && yarn test-internal && yarn test-react",
"test-coverage-most": "./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover ./lib/multi-runner.js --dir coverage.most && ./node_modules/.bin/remap-istanbul -i coverage.most/coverage.json -o coverage-sourcemapped -t html",
"test-all-coverage": "./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover ./lib/multi-runner.js --dir coverage.most && ./node_modules/.bin/istanbul --stack_size=10000 --max_old_space_size=16384 cover -- ./lib/test262-runner.js --timeout 50 --singleThreaded && ./node_modules/.bin/remap-istanbul -i coverage/coverage.json -i coverage.most/coverage.json -o coverage-sourcemapped -t html",
"repl": "node lib/repl-cli.js",

View File

@ -71,6 +71,14 @@ export default class ValuesDomain {
return true;
}
containsValue(x: Value): boolean {
let elems = this._elements;
if (elems === undefined) return true; // Top contains everything
if (x instanceof AbstractValue) return this.contains(x.values);
invariant(x instanceof ConcreteValue);
return elems.has(x);
}
isTop() {
return this._elements === undefined;
}

View File

@ -1352,7 +1352,7 @@ export class Reference {
);
this.base = base;
this.referencedName = refName;
invariant(!(refName instanceof AbstractValue) || !refName.mightNotBeString());
invariant(!(refName instanceof AbstractValue) || !(refName.mightNotBeString() && refName.mightNotBeNumber()));
this.strict = strict;
this.thisValue = thisValue;
invariant(thisValue === undefined || !(base instanceof EnvironmentRecord));

View File

@ -33,10 +33,6 @@ import {
Value,
} from "../values/index.js";
import invariant from "../invariant.js";
import buildExpressionTemplate from "../utils/builder.js";
const tsTemplateSrc = "(A).toString()";
const tsTemplate = buildExpressionTemplate(tsTemplateSrc);
type ElementConvType = {
Int8: (Realm, numberOrValue) => number,
@ -773,12 +769,7 @@ export class ToImplementation {
ToPropertyKeyPartial(realm: Realm, arg: Value): AbstractValue | SymbolValue | string /* but not StringValue */ {
if (arg instanceof ConcreteValue) return this.ToPropertyKey(realm, arg);
if (arg.mightNotBeString()) {
if (!arg.mightNotBeNumber()) {
return AbstractValue.createFromTemplate(realm, tsTemplate, StringValue, [arg], tsTemplateSrc);
}
arg.throwIfNotConcrete();
}
if (arg.mightNotBeString() && arg.mightNotBeNumber()) arg.throwIfNotConcrete();
invariant(arg instanceof AbstractValue);
return arg;
}

View File

@ -26,7 +26,7 @@ import { AbruptCompletion, PossiblyNormalCompletion } from "../completions.js";
import { Reference } from "../environment.js";
import { cloneDescriptor, equalDescriptors, IsDataDescriptor, StrictEqualityComparison } from "../methods/index.js";
import { Generator } from "../utils/generator.js";
import { AbstractValue, EmptyValue, ObjectValue, Value } from "../values/index.js";
import { AbstractValue, ArrayValue, EmptyValue, ObjectValue, Value } from "../values/index.js";
import invariant from "../invariant.js";
import * as t from "babel-types";
@ -236,12 +236,17 @@ export class WidenImplementation {
// For now, we only handle loop invariant properties
//i.e. properties where the member expresssion does not involve any values written to inside the loop.
//todo: handle the case where key is an abstract value.
let key = b.key;
if (typeof key === "string") {
pathNode = AbstractValue.createFromWidenedProperty(realm, rval, [b.object], ([o]) =>
t.memberExpression(o, t.identifier(key))
);
if (typeof key === "string" || !(key.mightNotBeString() && key.mightNotBeNumber())) {
if (typeof key === "string") {
pathNode = AbstractValue.createFromWidenedProperty(realm, rval, [b.object], ([o]) =>
t.memberExpression(o, t.identifier(key))
);
} else {
pathNode = AbstractValue.createFromWidenedProperty(realm, rval, [b.object, key], ([o, p]) => {
return t.memberExpression(o, p, true);
});
}
// The value of the property at the start of the loop needs to be written to the property
// before the loop commences, otherwise the memberExpression will result in an undefined value.
let generator = realm.generator;
@ -249,9 +254,17 @@ export class WidenImplementation {
let initVal = (b.descriptor && b.descriptor.value) || realm.intrinsics.empty;
if (!(initVal instanceof Value)) throw new FatalError("todo: handle internal properties");
if (!(initVal instanceof EmptyValue)) {
generator.emitVoidExpression(rval.types, rval.values, [b.object, initVal], ([o, v]) =>
t.assignmentExpression("=", t.memberExpression(o, t.identifier(key)), v)
);
if (key === "length" && b.object instanceof ArrayValue) {
// do nothing, the array length will already be initialized
} else if (typeof key === "string") {
generator.emitVoidExpression(rval.types, rval.values, [b.object, initVal], ([o, v]) =>
t.assignmentExpression("=", t.memberExpression(o, t.identifier(key)), v)
);
} else {
generator.emitVoidExpression(rval.types, rval.values, [b.object, b.key, initVal], ([o, p, v]) =>
t.assignmentExpression("=", t.memberExpression(o, p, true), v)
);
}
}
} else {
throw new FatalError("todo: handle the case where key is an abstract value");
@ -390,9 +403,9 @@ export class WidenImplementation {
}
_containsValues(val1: Value, val2: Value) {
if (val1 instanceof AbstractValue && val2 instanceof AbstractValue) {
if (val1.getType() !== val2.getType()) return false;
return val1.values.contains(val2.values);
if (val1 instanceof AbstractValue) {
if (!Value.isTypeCompatibleWith(val2.getType(), val1.getType())) return false;
return val1.values.containsValue(val2);
}
return val1.equals(val2);
}

View File

@ -572,8 +572,8 @@ export class Realm {
// result in any more values being added to abstract domains and hence a fixpoint has been reached.
// Generate code using effects2 because its expressions have not been widened away.
let [, gen, bindings2, pbindings2] = effects2;
this._emitLocalAssignments(gen, bindings2);
this._emitPropertAssignments(gen, pbindings2);
this._emitLocalAssignments(gen, bindings2);
return [effects1, effects2];
}
effects1 = Widen.widenEffects(this, effects1, effects2);
@ -634,7 +634,7 @@ export class Realm {
[key.object, value],
([o, n]) => {
invariant(value instanceof Value);
if (value.mightHaveBeenDeleted() && isSelfReferential(value, key.pathNode)) {
if (typeof key.key === "string" && value.mightHaveBeenDeleted() && isSelfReferential(value, key.pathNode)) {
let inTest = t.binaryExpression("in", t.stringLiteral(key.key), o);
let addEmpty = t.conditionalExpression(inTest, n, emptyExpression);
n = t.logicalExpression("||", n, addEmpty);
@ -651,28 +651,34 @@ export class Realm {
pbindings.forEach((val, key, map) => {
let path = key.pathNode;
let tval = tvalFor.get(key);
invariant(tval !== undefined);
invariant(val !== undefined);
let value = val.value;
invariant(value instanceof Value);
let mightHaveBeenDeleted = value.mightHaveBeenDeleted();
let mightBeUndefined = value.mightBeUndefined();
gen.emitStatement([key.object, tval, this.intrinsics.empty], ([o, v, e]) => {
invariant(path !== undefined);
let lh = path.buildNode([o]);
let r = t.expressionStatement(t.assignmentExpression("=", (lh: any), v));
if (mightHaveBeenDeleted) {
invariant(typeof key.key === "string"); // for now
// If v === __empty || (v === undefined && !(key.key in o)) then delete it
let emptyTest = t.binaryExpression("===", v, e);
let undefinedTest = t.binaryExpression("===", v, voidExpression);
let inTest = t.unaryExpression("!", t.binaryExpression("in", t.stringLiteral(key.key), o));
let guard = t.logicalExpression("||", emptyTest, t.logicalExpression("&&", undefinedTest, inTest));
let deleteIt = t.expressionStatement(t.unaryExpression("delete", (lh: any)));
return t.ifStatement(mightBeUndefined ? emptyTest : guard, deleteIt, r);
}
return r;
});
if (typeof key.key === "string") {
gen.emitStatement([key.object, tval || value, this.intrinsics.empty], ([o, v, e]) => {
invariant(path !== undefined);
let lh = path.buildNode([o, t.identifier(key.key)]);
let r = t.expressionStatement(t.assignmentExpression("=", (lh: any), v));
if (mightHaveBeenDeleted) {
// If v === __empty || (v === undefined && !(key.key in o)) then delete it
let emptyTest = t.binaryExpression("===", v, e);
let undefinedTest = t.binaryExpression("===", v, voidExpression);
let inTest = t.unaryExpression("!", t.binaryExpression("in", t.stringLiteral(key.key), o));
let guard = t.logicalExpression("||", emptyTest, t.logicalExpression("&&", undefinedTest, inTest));
let deleteIt = t.expressionStatement(t.unaryExpression("delete", (lh: any)));
return t.ifStatement(mightBeUndefined ? emptyTest : guard, deleteIt, r);
}
return r;
});
} else {
gen.emitStatement([key.object, key.key, tval || value, this.intrinsics.empty], ([o, p, v, e]) => {
invariant(path !== undefined);
let lh = path.buildNode([o, p]);
return t.expressionStatement(t.assignmentExpression("=", (lh: any), v));
});
}
});
}

View File

@ -331,6 +331,7 @@ export class ResidualHeapSerializer {
}
_getNestedAbstractValues(absVal: AbstractValue, values: Array<Value>): Array<Value> {
if (absVal.kind === "widened property") return values;
invariant(absVal.args.length === 3);
let cond = absVal.args[0];
invariant(cond instanceof AbstractValue);
@ -356,6 +357,7 @@ export class ResidualHeapSerializer {
}
_emitPropertiesWithComputedNames(obj: ObjectValue, absVal: AbstractValue) {
if (absVal.kind === "widened property") return;
invariant(absVal.args.length === 3);
let cond = absVal.args[0];
invariant(cond instanceof AbstractValue);
@ -870,15 +872,17 @@ export class ResidualHeapSerializer {
// 2. array length is concrete, but different from number of index properties
// we put into initialization list.
if (lenProperty instanceof AbstractValue || To.ToLength(realm, lenProperty) !== numberOfIndexProperties) {
this.emitter.emitNowOrAfterWaitingForDependencies([val], () => {
this._assignProperty(
() => t.memberExpression(this.getSerializeObjectIdentifier(val), t.identifier("length")),
() => {
return this.serializeValue(lenProperty);
},
false /*mightHaveBeenDeleted*/
);
});
if (!(lenProperty instanceof AbstractValue) || lenProperty.kind !== "widened property") {
this.emitter.emitNowOrAfterWaitingForDependencies([val], () => {
this._assignProperty(
() => t.memberExpression(this.getSerializeObjectIdentifier(val), t.identifier("length")),
() => {
return this.serializeValue(lenProperty);
},
false /*mightHaveBeenDeleted*/
);
});
}
remainingProperties.delete("length");
}
}

View File

@ -211,6 +211,7 @@ export class ResidualHeapVisitor {
}
visitObjectPropertiesWithComputedNames(absVal: AbstractValue): void {
if (absVal.kind === "widened property") return;
invariant(absVal.args.length === 3);
let cond = absVal.args[0];
invariant(cond instanceof AbstractValue);

View File

@ -47,6 +47,16 @@ import {
import { Join, Properties } from "../singletons.js";
import invariant from "../invariant.js";
import type { typeAnnotation } from "babel-types";
import * as t from "babel-types";
function isWidenedValue(v: void | Value) {
if (!(v instanceof AbstractValue)) return false;
if (v.kind === "widened" || v.kind === "widened property") return true;
for (let a of v.args) {
if (isWidenedValue(a)) return true;
}
return false;
}
export default class ObjectValue extends ConcreteValue {
constructor(
@ -541,13 +551,18 @@ export default class ObjectValue extends ConcreteValue {
$GetPartial(P: AbstractValue | PropertyKeyValue, Receiver: Value): Value {
if (!(P instanceof AbstractValue)) return this.$Get(P, Receiver);
// We assume that simple objects have no getter/setter properties.
if (this !== Receiver || !this.isSimpleObject() || P.mightNotBeString()) {
if (this !== Receiver || !this.isSimpleObject() || (P.mightNotBeString() && P.mightNotBeNumber())) {
AbstractValue.reportIntrospectionError(P, "TODO: #1021");
throw new FatalError();
}
// If all else fails, use this expression
let result;
if (this.isPartialObject()) {
if (isWidenedValue(P)) {
return AbstractValue.createTemporalFromBuildFunction(this.$Realm, Value, [this, P], ([o, p]) =>
t.memberExpression(o, p, true)
);
}
result = AbstractValue.createFromType(this.$Realm, Value, "sentinel member expression");
result.args = [this, P];
} else {
@ -586,6 +601,17 @@ export default class ObjectValue extends ConcreteValue {
}
specializeJoin(absVal: AbstractValue, propName: Value): Value {
if (absVal.kind === "widened property") {
let ob = absVal.args[0];
if (propName instanceof StringValue) {
let pName = propName.value;
let pNumber = +pName;
if (pName === pNumber + "") propName = new NumberValue(this.$Realm, pNumber);
}
return AbstractValue.createTemporalFromBuildFunction(this.$Realm, absVal.getType(), [ob, propName], ([o, p]) => {
return t.memberExpression(o, p, true);
});
}
invariant(absVal.args.length === 3 && absVal.kind === "conditional");
let generic_cond = absVal.args[0];
invariant(generic_cond instanceof AbstractValue);
@ -611,6 +637,8 @@ export default class ObjectValue extends ConcreteValue {
$SetPartial(P: AbstractValue | PropertyKeyValue, V: Value, Receiver: Value): boolean {
if (!(P instanceof AbstractValue)) return this.$Set(P, V, Receiver);
let pIsLoopVar = isWidenedValue(P);
let pIsNumeric = Value.isTypeCompatibleWith(P.getType(), NumberValue);
function createTemplate(realm: Realm, propName: AbstractValue) {
return AbstractValue.createFromBinaryOp(
@ -625,7 +653,7 @@ export default class ObjectValue extends ConcreteValue {
// We assume that simple objects have no getter/setter properties and
// that all properties are writable.
if (this !== Receiver || !this.isSimpleObject() || P.mightNotBeString()) {
if (this !== Receiver || !this.isSimpleObject() || (P.mightNotBeString() && P.mightNotBeNumber())) {
AbstractValue.reportIntrospectionError(P, "TODO #1021");
throw new FatalError();
}
@ -635,7 +663,7 @@ export default class ObjectValue extends ConcreteValue {
prop = {
descriptor: undefined,
object: this,
key: "",
key: P,
};
this.unknownProperty = prop;
} else {
@ -645,7 +673,7 @@ export default class ObjectValue extends ConcreteValue {
let desc = prop.descriptor;
if (desc === undefined) {
let newVal = V;
if (!(V instanceof UndefinedValue)) {
if (!(V instanceof UndefinedValue) && !isWidenedValue(P)) {
// join V with undefined, using a property name test as the condition
let cond = createTemplate(this.$Realm, P);
newVal = Join.joinValuesAsConditional(this.$Realm, cond, V, this.$Realm.intrinsics.undefined);
@ -662,15 +690,28 @@ export default class ObjectValue extends ConcreteValue {
invariant(oldVal instanceof Value);
let newVal = oldVal;
if (!(V instanceof UndefinedValue)) {
let cond = createTemplate(this.$Realm, P);
newVal = Join.joinValuesAsConditional(this.$Realm, cond, V, oldVal);
if (isWidenedValue(P)) {
newVal = V; // It will be widened later on
} else {
let cond = createTemplate(this.$Realm, P);
newVal = Join.joinValuesAsConditional(this.$Realm, cond, V, oldVal);
}
}
desc.value = newVal;
}
// Since we don't know the name of the property we are writing to, we also need
// to perform weak updates of all of the known properties.
// First clear out this.unknownProperty so that helper routines know its OK to update the properties
let savedUnknownProperty = this.unknownProperty;
this.unknownProperty = undefined;
for (let [key, propertyBinding] of this.properties) {
if (pIsLoopVar && pIsNumeric) {
// Delete numeric properties and don't do weak updates on other properties.
if (key !== +key + "") continue;
this.properties.delete(key);
continue;
}
let oldVal = this.$Realm.intrinsics.empty;
if (propertyBinding.descriptor && propertyBinding.descriptor.value) {
oldVal = propertyBinding.descriptor.value;
@ -680,6 +721,7 @@ export default class ObjectValue extends ConcreteValue {
let newVal = Join.joinValuesAsConditional(this.$Realm, cond, V, oldVal);
Properties.OrdinarySet(this.$Realm, this, key, newVal, Receiver);
}
this.unknownProperty = savedUnknownProperty;
return true;
}

View File

@ -0,0 +1,10 @@
let n = global.__abstract ? __abstract("number", "10") : 10;
let i = 0;
let ob = { };
do {
i++;
ob[i] = 1;
} while (i < n);
let x = ob[5];
inspect = function() { return i + " " + x + " " + JSON.stringify(ob); }

View File

@ -0,0 +1,11 @@
let a = global.__abstract ? __abstract([], "[1, 2, 3]") : [1, 2, 3];
let n = global.__abstract ? __abstract("number", "(3)") : 3;
let i = 0;
let b = [];
do {
b[i] = a[i++];
} while (i < n);
let x = b[1];
let j = b.length;
inspect = function() { return i + " " + j + " " + x + " " + JSON.stringify(b); }