Adds support for abstract length arrays in React reconcilation and serialization (#2571)

Summary:
Release notes: none

This PR fixes issues with the React hoisting and equivalence system mechanics and serialization where previous, there was no support for abstract length arrays. This includes an optimization for when the React serializer outputs ReactElement children that are conditionals with one side being an empty value (in this case, we can use an empty string instead).

Tests attached that focus on the areas covered in this PR.
Pull Request resolved: https://github.com/facebook/prepack/pull/2571

Differential Revision: D10082133

Pulled By: trueadm

fbshipit-source-id: d7de1834e10a5c4b3f35a90b9676ec72c6e797e2
This commit is contained in:
Dominic Gannaway 2018-09-27 00:32:21 -07:00 committed by Facebook Github Bot
parent 9681e2eeee
commit 47cb48b438
11 changed files with 592 additions and 51 deletions

View File

@ -216,22 +216,28 @@ export class ReactEquivalenceSet {
return ((map.get(result): any): ReactSetNode);
}
// for arrays: [0] -> [1] -> [2]... as nodes
// for arrays: [length] -> ([length] is numeric) -> [0] -> [1] -> [2]... as nodes
_getArrayValue(array: ArrayValue, visitedValues: Set<Value>): ArrayValue {
if (visitedValues.has(array)) return array;
if (array.intrinsicName) return array;
visitedValues.add(array);
let currentMap = this.arrayRoot;
currentMap = this.getKey("length", currentMap, visitedValues);
let result = this.getEquivalentPropertyValue(array, "length", currentMap, visitedValues);
currentMap = result.map;
let lengthValue = getProperty(this.realm, array, "length");
// If we have a numeric lenth that is not abstract, then also check all the array elements
if (lengthValue instanceof NumberValue) {
invariant(lengthValue instanceof NumberValue);
let length = lengthValue.value;
let currentMap = this.arrayRoot;
let result;
for (let i = 0; i < length; i++) {
currentMap = this.getKey(i, currentMap, visitedValues);
result = this.getEquivalentPropertyValue(array, "" + i, currentMap, visitedValues);
currentMap = result.map;
}
}
if (result === undefined) {
if (this.realm.react.emptyArray !== undefined) {
return this.realm.react.emptyArray;
@ -241,6 +247,7 @@ export class ReactEquivalenceSet {
if (result.value === null) {
result.value = array;
}
invariant(result.value instanceof ArrayValue);
return result.value;
}

View File

@ -28,6 +28,7 @@ import {
createInternalReactElement,
flagPropsWithNoPartialKeyOrRef,
flattenChildren,
getMaxLength,
hardModifyReactObjectPropertyBinding,
getProperty,
hasNoPartialKeyOrRef,
@ -418,7 +419,7 @@ export function wrapReactElementWithKeyedFragment(realm: Realm, keyValue: Value,
Create.CreateDataPropertyOrThrow(realm, fragmentConfigValue, "key", keyValue);
let fragmentChildrenValue = Create.ArrayCreate(realm, 1);
Create.CreateDataPropertyOrThrow(realm, fragmentChildrenValue, "0", reactElement);
fragmentChildrenValue = flattenChildren(realm, fragmentChildrenValue);
fragmentChildrenValue = flattenChildren(realm, fragmentChildrenValue, true);
return createReactElement(realm, reactFragment, fragmentConfigValue, fragmentChildrenValue);
}
@ -449,6 +450,13 @@ export function traverseReactElement(
traversalVisitor.visitRef(refValue);
}
const loopArrayElements = (childrenValue: ArrayValue, length: number): void => {
for (let i = 0; i < length; i++) {
let child = getProperty(realm, childrenValue, "" + i);
traversalVisitor.visitChildNode(child);
}
};
const handleChildren = () => {
// handle children
invariant(propsValue instanceof ObjectValue);
@ -457,13 +465,12 @@ export function traverseReactElement(
if (childrenValue !== realm.intrinsics.undefined && childrenValue !== realm.intrinsics.null) {
if (childrenValue instanceof ArrayValue && !childrenValue.intrinsicName) {
let childrenLength = getProperty(realm, childrenValue, "length");
let childrenLengthValue = 0;
if (childrenLength instanceof NumberValue) {
childrenLengthValue = childrenLength.value;
for (let i = 0; i < childrenLengthValue; i++) {
let child = getProperty(realm, childrenValue, "" + i);
traversalVisitor.visitChildNode(child);
}
loopArrayElements(childrenValue, childrenLength.value);
} else if (childrenLength instanceof AbstractValue && childrenLength.kind === "conditional") {
loopArrayElements(childrenValue, getMaxLength(childrenLength, 0));
} else {
invariant(false, "TODO: support other types of array length value");
}
} else {
traversalVisitor.visitChildNode(childrenValue);

View File

@ -61,7 +61,10 @@ function canHoistArray(
): boolean {
if (array.intrinsicName) return false;
let lengthValue = Get(realm, array, "length");
invariant(lengthValue instanceof NumberValue);
if (!canHoistValue(realm, lengthValue, residualHeapVisitor, visitedValues)) {
return false;
}
if (lengthValue instanceof NumberValue) {
let length = lengthValue.value;
for (let i = 0; i < length; i++) {
let element = Get(realm, array, "" + i);
@ -70,6 +73,7 @@ function canHoistArray(
return false;
}
}
}
return true;
}

View File

@ -1033,7 +1033,7 @@ export class Reconciler {
);
// we can optimize further and flatten arrays on non-composite components
if (resolvedChildren instanceof ArrayValue && !resolvedChildren.intrinsicName) {
resolvedChildren = flattenChildren(this.realm, resolvedChildren);
resolvedChildren = flattenChildren(this.realm, resolvedChildren, true);
}
if (resolvedChildren !== childrenValue) {
let newProps = cloneProps(this.realm, propsValue, resolvedChildren);

View File

@ -21,6 +21,7 @@ import {
BoundFunctionValue,
ECMAScriptFunctionValue,
ECMAScriptSourceFunctionValue,
EmptyValue,
FunctionValue,
NumberValue,
ObjectValue,
@ -239,17 +240,31 @@ export function forEachArrayValue(
mapFunc: (element: Value, index: number) => void
): void {
let lengthValue = Get(realm, array, "length");
invariant(lengthValue instanceof NumberValue, "TODO: support non-numeric length on forEachArrayValue");
let length = lengthValue.value;
let isConditionalLength = lengthValue instanceof AbstractValue && lengthValue.kind === "conditional";
let length;
if (isConditionalLength) {
length = getMaxLength(lengthValue, 0);
} else {
invariant(lengthValue instanceof NumberValue, "TODO: support other types of array length value");
length = lengthValue.value;
}
for (let i = 0; i < length; i++) {
let elementProperty = array.properties.get("" + i);
let elementPropertyDescriptor = elementProperty && elementProperty.descriptor;
if (elementPropertyDescriptor) {
invariant(elementPropertyDescriptor instanceof PropertyDescriptor);
let elementValue = elementPropertyDescriptor.value;
if (elementValue instanceof Value) {
mapFunc(elementValue, i);
// If we are in an array with conditional length, the element might be a conditional join
// of the same type as the length of the array
if (isConditionalLength && elementValue instanceof AbstractValue && elementValue.kind === "conditional") {
invariant(lengthValue instanceof AbstractValue);
let lengthCondition = lengthValue.args[0];
let elementCondition = elementValue.args[0];
// If they are the same condition
invariant(lengthCondition.equals(elementCondition), "TODO: support cases where the condition is not the same");
}
invariant(elementValue instanceof Value);
mapFunc(elementValue, i);
}
}
}
@ -259,11 +274,11 @@ export function mapArrayValue(
array: ArrayValue,
mapFunc: (element: Value, descriptor: Descriptor) => Value
): ArrayValue {
let lengthValue = Get(realm, array, "length");
invariant(lengthValue instanceof NumberValue, "TODO: support non-numeric length on mapArrayValue");
let length = lengthValue.value;
let newArray = Create.ArrayCreate(realm, length);
let returnTheNewArray = false;
let newArray;
const mapArray = (lengthValue: NumberValue): void => {
let length = lengthValue.value;
for (let i = 0; i < length; i++) {
let elementProperty = array.properties.get("" + i);
@ -282,6 +297,44 @@ export function mapArrayValue(
}
Create.CreateDataPropertyOrThrow(realm, newArray, "" + i, realm.intrinsics.undefined);
}
};
let lengthValue = Get(realm, array, "length");
if (lengthValue instanceof AbstractValue && lengthValue.kind === "conditional") {
returnTheNewArray = true;
let [condValue, consequentVal, alternateVal] = lengthValue.args;
newArray = Create.ArrayCreate(realm, 0);
realm.evaluateWithAbstractConditional(
condValue,
() => {
return realm.evaluateForEffects(
() => {
invariant(consequentVal instanceof NumberValue);
mapArray(consequentVal);
return realm.intrinsics.undefined;
},
null,
"mapArrayValue consequent"
);
},
() => {
return realm.evaluateForEffects(
() => {
invariant(alternateVal instanceof NumberValue);
mapArray(alternateVal);
return realm.intrinsics.undefined;
},
null,
"mapArrayValue alternate"
);
}
);
} else if (lengthValue instanceof NumberValue) {
newArray = Create.ArrayCreate(realm, lengthValue.value);
mapArray(lengthValue);
} else {
invariant(false, "TODO: support other types of array length value");
}
return returnTheNewArray ? newArray : array;
}
@ -602,21 +655,68 @@ export function hasNoPartialKeyOrRef(realm: Realm, props: ObjectValue | Abstract
return false;
}
function recursivelyFlattenArray(realm: Realm, array, targetArray): void {
forEachArrayValue(realm, array, item => {
if (item instanceof ArrayValue && !item.intrinsicName) {
recursivelyFlattenArray(realm, item, targetArray);
export function getMaxLength(value: Value, maxLength: number): number {
if (value instanceof NumberValue) {
if (value.value > maxLength) {
return value.value;
} else {
return maxLength;
}
} else if (value instanceof AbstractValue && value.kind === "conditional") {
let [, consequentVal, alternateVal] = value.args;
let consequentMaxVal = getMaxLength(consequentVal, maxLength);
let alternateMaxVal = getMaxLength(alternateVal, maxLength);
if (consequentMaxVal > maxLength && consequentMaxVal >= alternateMaxVal) {
return consequentMaxVal;
} else if (alternateMaxVal > maxLength && alternateMaxVal >= consequentMaxVal) {
return alternateMaxVal;
}
return maxLength;
}
invariant(false, "TODO: support other types of array length value");
}
function recursivelyFlattenArray(realm: Realm, array, targetArray: ArrayValue, noHoles: boolean): void {
forEachArrayValue(realm, array, _item => {
let element = _item;
if (element instanceof ArrayValue && !element.intrinsicName) {
recursivelyFlattenArray(realm, element, targetArray, noHoles);
} else {
let lengthValue = Get(realm, targetArray, "length");
invariant(lengthValue instanceof NumberValue);
Properties.Set(realm, targetArray, "" + lengthValue.value, item, true);
if (noHoles && element instanceof EmptyValue) {
// We skip holely elements
return;
} else if (noHoles && element instanceof AbstractValue && element.kind === "conditional") {
let [condValue, consequentVal, alternateVal] = element.args;
invariant(condValue instanceof AbstractValue);
let consquentIsHolely = consequentVal instanceof EmptyValue;
let alternateIsHolely = alternateVal instanceof EmptyValue;
if (consquentIsHolely && alternateIsHolely) {
// We skip holely elements
return;
}
if (consquentIsHolely) {
element = AbstractValue.createFromLogicalOp(
realm,
"&&",
AbstractValue.createFromUnaryOp(realm, "!", condValue),
alternateVal
);
}
if (alternateIsHolely) {
element = AbstractValue.createFromLogicalOp(realm, "&&", condValue, consequentVal);
}
}
Properties.Set(realm, targetArray, "" + lengthValue.value, element, true);
}
});
}
export function flattenChildren(realm: Realm, array: ArrayValue): ArrayValue {
export function flattenChildren(realm: Realm, array: ArrayValue, noHoles: boolean): ArrayValue {
let flattenedChildren = Create.ArrayCreate(realm, 0);
recursivelyFlattenArray(realm, array, flattenedChildren);
recursivelyFlattenArray(realm, array, flattenedChildren, noHoles);
flattenedChildren.makeFinal();
return flattenedChildren;
}

View File

@ -132,6 +132,22 @@ it("Simple 25", () => {
runTest(__dirname + "/FunctionalComponents/simple-25.js");
});
it("Simple 26", () => {
runTest(__dirname + "/FunctionalComponents/simple-26.js");
});
it("Simple 27", () => {
runTest(__dirname + "/FunctionalComponents/simple-27.js");
});
it("Simple 28", () => {
runTest(__dirname + "/FunctionalComponents/simple-28.js");
});
it("Simple 29", () => {
runTest(__dirname + "/FunctionalComponents/simple-29.js");
});
it("Bound type", () => {
runTest(__dirname + "/FunctionalComponents/bound-type.js");
});

View File

@ -0,0 +1,32 @@
var React = require("react");
function App(props) {
var arr = [1, 2, 3];
if (props.cond) {
arr.push(4);
} else {
arr.pop();
}
return (
<div>
<span x={arr} />
<span x={arr} />
</div>
);
}
App.getTrials = function(renderer, Root) {
let results = [];
renderer.update(<Root cond={true} />);
results.push(["abstract array length on prop (cond: true)", renderer.toJSON()]);
renderer.update(<Root cond={false} />);
results.push(["abstract array length on prop (cond: false)", renderer.toJSON()]);
return results;
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
module.exports = App;

View File

@ -0,0 +1,32 @@
var React = require("react");
function App(props) {
var arr = [1, 2, 3];
if (props.cond) {
arr.push(4);
} else {
arr.pop();
}
return (
<div>
<span>{arr}</span>
<span>{arr}</span>
</div>
);
}
App.getTrials = function(renderer, Root) {
let results = [];
renderer.update(<Root cond={true} />);
results.push(["abstract array length on prop (cond: true)", renderer.toJSON()]);
renderer.update(<Root cond={false} />);
results.push(["abstract array length on prop (cond: false)", renderer.toJSON()]);
return results;
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
module.exports = App;

View File

@ -0,0 +1,34 @@
var React = require("react");
function App(props) {
var arr = [1, 2, 3];
if (props.cond) {
arr.push(4);
} else {
arr.pop();
}
arr.reverse();
return (
<div>
<span>{arr[0]}</span>
<span>{arr[1]}</span>
<span>{arr[2]}</span>
</div>
);
}
App.getTrials = function(renderer, Root) {
let results = [];
renderer.update(<Root cond={true} />);
results.push(["abstract array length on prop (cond: true)", renderer.toJSON()]);
renderer.update(<Root cond={false} />);
results.push(["abstract array length on prop (cond: false)", renderer.toJSON()]);
return results;
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
module.exports = App;

View File

@ -0,0 +1,37 @@
var React = require("react");
function App(props) {
var arr = [1, 2, 3];
if (props.cond) {
arr.push(4);
} else {
arr.pop();
}
var arr1 = Array.from(arr);
arr1.reverse();
var arr2 = Array.from(arr).join(", ");
var arr3 = Array.from(arr).map(x => x);
return (
<div>
<span>{arr1}</span>
<span>{arr2}</span>
<span>{arr3}</span>
</div>
);
}
App.getTrials = function(renderer, Root) {
let results = [];
renderer.update(<Root cond={true} />);
results.push(["abstract array length on prop (cond: true)", renderer.toJSON()]);
renderer.update(<Root cond={false} />);
results.push(["abstract array length on prop (cond: false)", renderer.toJSON()]);
return results;
};
if (this.__optimizeReactComponentTree) {
__optimizeReactComponentTree(App);
}
module.exports = App;

View File

@ -11568,6 +11568,278 @@ ReactStatistics {
}
`;
exports[`Simple 26: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 26: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 26: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 26: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 27: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 27: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 27: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 27: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 28: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 28: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 28: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 28: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 29: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 29: (JSX => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 29: (createElement => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple 29: (createElement => createElement) 1`] = `
ReactStatistics {
"componentsEvaluated": 1,
"evaluatedRootNodes": Array [
Object {
"children": Array [],
"message": "",
"name": "App",
"status": "ROOT",
},
],
"inlinedComponents": 0,
"optimizedNestedClosures": 0,
"optimizedTrees": 1,
}
`;
exports[`Simple children: (JSX => JSX) 1`] = `
ReactStatistics {
"componentsEvaluated": 3,