diff --git a/package.json b/package.json index 6c1497001..89156f25b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/domains/ValuesDomain.js b/src/domains/ValuesDomain.js index b295941b8..a1b095db5 100644 --- a/src/domains/ValuesDomain.js +++ b/src/domains/ValuesDomain.js @@ -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; } diff --git a/src/environment.js b/src/environment.js index f6e566356..777143611 100644 --- a/src/environment.js +++ b/src/environment.js @@ -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)); diff --git a/src/methods/to.js b/src/methods/to.js index 904c9ab68..3d973f477 100644 --- a/src/methods/to.js +++ b/src/methods/to.js @@ -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; } diff --git a/src/methods/widen.js b/src/methods/widen.js index 96f57e1a2..4e933c8e0 100644 --- a/src/methods/widen.js +++ b/src/methods/widen.js @@ -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); } diff --git a/src/realm.js b/src/realm.js index 925c36b1c..db89f23a7 100644 --- a/src/realm.js +++ b/src/realm.js @@ -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)); + }); + } }); } diff --git a/src/serializer/ResidualHeapSerializer.js b/src/serializer/ResidualHeapSerializer.js index 5793886c3..e487b2e6f 100644 --- a/src/serializer/ResidualHeapSerializer.js +++ b/src/serializer/ResidualHeapSerializer.js @@ -331,6 +331,7 @@ export class ResidualHeapSerializer { } _getNestedAbstractValues(absVal: AbstractValue, values: Array): Array { + 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"); } } diff --git a/src/serializer/ResidualHeapVisitor.js b/src/serializer/ResidualHeapVisitor.js index 2795b9ee3..5819c9d54 100644 --- a/src/serializer/ResidualHeapVisitor.js +++ b/src/serializer/ResidualHeapVisitor.js @@ -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); diff --git a/src/values/ObjectValue.js b/src/values/ObjectValue.js index 3d817b2d3..d34d158c3 100644 --- a/src/values/ObjectValue.js +++ b/src/values/ObjectValue.js @@ -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; } diff --git a/test/serializer/abstract/DoWhile7.js b/test/serializer/abstract/DoWhile7.js new file mode 100644 index 000000000..812ddcd2c --- /dev/null +++ b/test/serializer/abstract/DoWhile7.js @@ -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); } diff --git a/test/serializer/abstract/DoWhile8.js b/test/serializer/abstract/DoWhile8.js new file mode 100644 index 000000000..f72c3b7c3 --- /dev/null +++ b/test/serializer/abstract/DoWhile8.js @@ -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); }