diff --git a/src/serializer/ResidualHeapSerializer.js b/src/serializer/ResidualHeapSerializer.js index b02b061ee..9c7305341 100644 --- a/src/serializer/ResidualHeapSerializer.js +++ b/src/serializer/ResidualHeapSerializer.js @@ -2300,7 +2300,7 @@ export class ResidualHeapSerializer { invariant(effectsGenerator === generator); effectsGenerator.serialize(this._getContext()); this.realm.withEffectsAppliedInGlobalEnv(() => { - const lazyHoistedReactNodes = this.residualReactElementSerializer.serializeLazyHoistedNodes(); + const lazyHoistedReactNodes = this.residualReactElementSerializer.serializeLazyHoistedNodes(functionValue); this.mainBody.entries.push(...lazyHoistedReactNodes); return null; }, additionalEffects.effects); diff --git a/src/serializer/ResidualHeapVisitor.js b/src/serializer/ResidualHeapVisitor.js index a94d3976a..696a0d4d6 100644 --- a/src/serializer/ResidualHeapVisitor.js +++ b/src/serializer/ResidualHeapVisitor.js @@ -66,6 +66,7 @@ import { ResidualReactElementVisitor } from "./ResidualReactElementVisitor.js"; import { GeneratorDAG } from "./GeneratorDAG.js"; export type Scope = FunctionValue | Generator; + type BindingState = {| capturedBindings: Set, capturingFunctions: Set, @@ -254,6 +255,14 @@ export class ResidualHeapVisitor { // Queues up an action to be later processed in some arbitrary scope. _enqueueWithUnrelatedScope(scope: Scope, action: () => void | boolean): void { + // If we are in a zone with a non-default equivalence set (we are wrapped in a `withCleanEquivalenceSet` call) then + // we need to save our equivalence set so that we may load it before running our action. + if (this.residualReactElementVisitor.defaultEquivalenceSet === false) { + const save = this.residualReactElementVisitor.saveEquivalenceSet(); + const originalAction = action; + action = () => this.residualReactElementVisitor.loadEquivalenceSet(save, originalAction); + } + this.delayedActions.push({ scope, action }); } @@ -1114,10 +1123,7 @@ export class ResidualHeapVisitor { this.referencedDeclaredValues.set(value, this._getAdditionalFunctionOfScope()); }, recordDelayedEntry: (generator, entry: GeneratorEntry) => { - this.delayedActions.push({ - scope: generator, - action: () => entry.visit(callbacks, generator), - }); + this._enqueueWithUnrelatedScope(generator, () => entry.visit(callbacks, generator)); }, visitModifiedProperty: (binding: PropertyBinding) => { let fixpoint_rerun = () => { @@ -1224,7 +1230,7 @@ export class ResidualHeapVisitor { // Set Visitor state // Allows us to emit function declarations etc. inside of this additional // function instead of adding them at global scope - this.residualReactElementVisitor.withCleanEquivalenceSet(() => { + let visitor = () => { invariant(funcInstance !== undefined); invariant(functionInfo !== undefined); let additionalFunctionInfo = { @@ -1238,7 +1244,13 @@ export class ResidualHeapVisitor { let effectsGenerator = additionalEffects.generator; this.generatorDAG.add(functionValue, effectsGenerator); this.visitGenerator(effectsGenerator, additionalFunctionInfo); - }); + }; + + if (this.realm.react.enabled) { + this.residualReactElementVisitor.withCleanEquivalenceSet(visitor); + } else { + visitor(); + } } visitRoots(): void { diff --git a/src/serializer/ResidualReactElementSerializer.js b/src/serializer/ResidualReactElementSerializer.js index 964f25ebc..0140e037c 100644 --- a/src/serializer/ResidualReactElementSerializer.js +++ b/src/serializer/ResidualReactElementSerializer.js @@ -14,7 +14,7 @@ import { ResidualHeapSerializer } from "./ResidualHeapSerializer.js"; import { canHoistReactElement } from "../react/hoisting.js"; import * as t from "@babel/types"; import type { BabelNode, BabelNodeExpression } from "@babel/types"; -import { AbstractValue, AbstractObjectValue, ObjectValue, SymbolValue, Value } from "../values/index.js"; +import { AbstractValue, AbstractObjectValue, ObjectValue, SymbolValue, FunctionValue, Value } from "../values/index.js"; import { convertExpressionToJSXIdentifier, convertKeyValueToJSXAttribute } from "../react/jsx.js"; import { Logger } from "../utils/logger.js"; import invariant from "../invariant.js"; @@ -52,14 +52,14 @@ export class ResidualReactElementSerializer { this.residualHeapSerializer = residualHeapSerializer; this.logger = residualHeapSerializer.logger; this.reactOutput = realm.react.output || "create-element"; - this._lazilyHoistedNodes = undefined; + this._lazilyHoistedNodes = new Map(); } realm: Realm; logger: Logger; reactOutput: ReactOutputTypes; residualHeapSerializer: ResidualHeapSerializer; - _lazilyHoistedNodes: void | LazilyHoistedNodes; + _lazilyHoistedNodes: Map; _createReactElement(value: ObjectValue): ReactElement { return { attributes: [], children: [], declared: false, type: undefined, value }; @@ -82,13 +82,17 @@ export class ResidualReactElementSerializer { ): void { // if the currentHoistedReactElements is not defined, we create it an emit the function call // this should only occur once per additional function - if (this._lazilyHoistedNodes === undefined) { + const optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(reactElement); + invariant(optimizedFunction); + let lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + if (lazilyHoistedNodes === undefined) { let funcId = t.identifier(this.residualHeapSerializer.functionNameGenerator.generate()); - this._lazilyHoistedNodes = { + lazilyHoistedNodes = { id: funcId, createElementIdentifier: hoistedCreateElementIdentifier, nodes: [], }; + this._lazilyHoistedNodes.set(optimizedFunction, lazilyHoistedNodes); let statement = t.expressionStatement( t.logicalExpression( "&&", @@ -97,14 +101,11 @@ export class ResidualReactElementSerializer { t.callExpression(funcId, originalCreateElementIdentifier ? [originalCreateElementIdentifier] : []) ) ); - let optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(reactElement); this.residualHeapSerializer.getPrelude(optimizedFunction).push(statement); } // we then push the reactElement and its id into our list of elements to process after // the current additional function has serialzied - invariant(this._lazilyHoistedNodes !== undefined); - invariant(Array.isArray(this._lazilyHoistedNodes.nodes)); - this._lazilyHoistedNodes.nodes.push({ id, astNode: reactElementAst }); + lazilyHoistedNodes.nodes.push({ id, astNode: reactElementAst }); } _getReactLibraryValue(): AbstractObjectValue | ObjectValue { @@ -155,15 +156,18 @@ export class ResidualReactElementSerializer { originalCreateElementIdentifier = this.residualHeapSerializer.serializeValue(createElement); if (shouldHoist) { - // if we haven't created a _lazilyHoistedNodes before, then this is the first time + const optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(value); + invariant(optimizedFunction); + const lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + // if we haven't created a lazilyHoistedNodes before, then this is the first time // so we only create the hoisted identifier once - if (this._lazilyHoistedNodes === undefined) { + if (lazilyHoistedNodes === undefined) { // create a new unique instance hoistedCreateElementIdentifier = t.identifier( this.residualHeapSerializer.intrinsicNameGenerator.generate() ); } else { - hoistedCreateElementIdentifier = this._lazilyHoistedNodes.createElementIdentifier; + hoistedCreateElementIdentifier = lazilyHoistedNodes.createElementIdentifier; } } @@ -438,10 +442,11 @@ export class ResidualReactElementSerializer { return reactElementChild; } - serializeLazyHoistedNodes(): Array { + serializeLazyHoistedNodes(optimizedFunction: FunctionValue): Array { const entries = []; - if (this._lazilyHoistedNodes !== undefined) { - let { id, nodes, createElementIdentifier } = this._lazilyHoistedNodes; + const lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + if (lazilyHoistedNodes !== undefined) { + let { id, nodes, createElementIdentifier } = lazilyHoistedNodes; // create a function that initializes all the hoisted nodes let func = t.functionExpression( null, @@ -454,7 +459,7 @@ export class ResidualReactElementSerializer { // output all the empty variable declarations that will hold the nodes lazily entries.push(...nodes.map(node => t.variableDeclaration("var", [t.variableDeclarator(node.id)]))); // reset the _lazilyHoistedNodes so other additional functions work - this._lazilyHoistedNodes = undefined; + this._lazilyHoistedNodes.delete(optimizedFunction); } return entries; } diff --git a/src/serializer/ResidualReactElementVisitor.js b/src/serializer/ResidualReactElementVisitor.js index 07cac48e8..75695e200 100644 --- a/src/serializer/ResidualReactElementVisitor.js +++ b/src/serializer/ResidualReactElementVisitor.js @@ -36,11 +36,18 @@ import { ReactPropsSet } from "../react/ReactPropsSet.js"; import type { ReactOutputTypes } from "../options.js"; import { Get } from "../methods/index.js"; +export opaque type ReactEquivalenceSetSave = {| + +reactEquivalenceSet: ReactEquivalenceSet, + +reactElementEquivalenceSet: ReactElementSet, + +reactPropsEquivalenceSet: ReactPropsSet, +|}; + export class ResidualReactElementVisitor { constructor(realm: Realm, residualHeapVisitor: ResidualHeapVisitor) { this.realm = realm; this.residualHeapVisitor = residualHeapVisitor; this.reactOutput = realm.react.output || "create-element"; + this.defaultEquivalenceSet = true; this.reactEquivalenceSet = new ReactEquivalenceSet(realm, this); this.reactElementEquivalenceSet = new ReactElementSet(realm, this.reactEquivalenceSet); this.reactPropsEquivalenceSet = new ReactPropsSet(realm, this.reactEquivalenceSet); @@ -49,6 +56,7 @@ export class ResidualReactElementVisitor { realm: Realm; residualHeapVisitor: ResidualHeapVisitor; reactOutput: ReactOutputTypes; + defaultEquivalenceSet: boolean; reactEquivalenceSet: ReactEquivalenceSet; reactElementEquivalenceSet: ReactElementSet; reactPropsEquivalenceSet: ReactPropsSet; @@ -138,19 +146,45 @@ export class ResidualReactElementVisitor { } withCleanEquivalenceSet(func: () => void): void { + let defaultEquivalenceSet = this.defaultEquivalenceSet; let reactEquivalenceSet = this.reactEquivalenceSet; let reactElementEquivalenceSet = this.reactElementEquivalenceSet; let reactPropsEquivalenceSet = this.reactPropsEquivalenceSet; + this.defaultEquivalenceSet = false; this.reactEquivalenceSet = new ReactEquivalenceSet(this.realm, this); this.reactElementEquivalenceSet = new ReactElementSet(this.realm, this.reactEquivalenceSet); this.reactPropsEquivalenceSet = new ReactPropsSet(this.realm, this.reactEquivalenceSet); func(); // Cleanup + this.defaultEquivalenceSet = defaultEquivalenceSet; this.reactEquivalenceSet = reactEquivalenceSet; this.reactElementEquivalenceSet = reactElementEquivalenceSet; this.reactPropsEquivalenceSet = reactPropsEquivalenceSet; } + saveEquivalenceSet(): ReactEquivalenceSetSave { + const { reactEquivalenceSet, reactElementEquivalenceSet, reactPropsEquivalenceSet } = this; + return { reactEquivalenceSet, reactElementEquivalenceSet, reactPropsEquivalenceSet }; + } + + loadEquivalenceSet(save: ReactEquivalenceSetSave, func: () => T): T { + const defaultEquivalenceSet = this.defaultEquivalenceSet; + const reactEquivalenceSet = this.reactEquivalenceSet; + const reactElementEquivalenceSet = this.reactElementEquivalenceSet; + const reactPropsEquivalenceSet = this.reactPropsEquivalenceSet; + this.defaultEquivalenceSet = false; + this.reactEquivalenceSet = save.reactEquivalenceSet; + this.reactElementEquivalenceSet = save.reactElementEquivalenceSet; + this.reactPropsEquivalenceSet = save.reactPropsEquivalenceSet; + const result = func(); + // Cleanup + this.defaultEquivalenceSet = defaultEquivalenceSet; + this.reactEquivalenceSet = reactEquivalenceSet; + this.reactElementEquivalenceSet = reactElementEquivalenceSet; + this.reactPropsEquivalenceSet = reactPropsEquivalenceSet; + return result; + } + wasTemporalAliasDeclaredInCurrentScope(temporalAlias: AbstractObjectValue): boolean { let scope = this.residualHeapVisitor.scope; if (scope instanceof FunctionValue) { diff --git a/test/react/ClassComponents-test.js b/test/react/ClassComponents-test.js index 2a0cb1e9a..f20445ec6 100644 --- a/test/react/ClassComponents-test.js +++ b/test/react/ClassComponents-test.js @@ -66,3 +66,11 @@ it("Complex class components folding into functional root component #4", () => { it("Complex class components folding into functional root component #5", () => { runTest(__dirname + "/ClassComponents/complex-class-into-functional-root5.js"); }); + +it("Complex class component rendering equivalent node to functional root component", () => { + runTest(__dirname + "/ClassComponents/complex-class-with-equivalent-node.js"); +}); + +it("Complex class component hoists nodes independently of functional root component", () => { + runTest(__dirname + "/ClassComponents/complex-class-proper-hoisting.js"); +}); diff --git a/test/react/ClassComponents/complex-class-proper-hoisting.js b/test/react/ClassComponents/complex-class-proper-hoisting.js new file mode 100644 index 000000000..6f4563699 --- /dev/null +++ b/test/react/ClassComponents/complex-class-proper-hoisting.js @@ -0,0 +1,43 @@ +const React = require("react"); + +function Tau(props) { + return React.createElement( + "a", + null, + React.createElement("b", null), + React.createElement(Epsilon, null), + React.createElement("c", null) + ); +} + +class Epsilon extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return React.createElement(React.Fragment, null, React.createElement("d", null), React.createElement("b", null)); + } +} + +if (this.__optimizeReactComponentTree) __optimizeReactComponentTree(Tau); + +Tau.getTrials = renderer => { + const trials = []; + + renderer.update(); + trials.push(["render Epsilon", renderer.toJSON()]); + + renderer.update(); + trials.push(["render Tau", renderer.toJSON()]); + + const a = Tau().props.children[0]; + const b = Epsilon.prototype.render.call(undefined).props.children[1]; + if (a.type !== "b" || b.type !== "b") throw new Error("Expected s"); + trials.push(["different React elements", JSON.stringify(a !== b)]); + + return trials; +}; + +module.exports = Tau; diff --git a/test/react/ClassComponents/complex-class-with-equivalent-node.js b/test/react/ClassComponents/complex-class-with-equivalent-node.js new file mode 100644 index 000000000..0b7874483 --- /dev/null +++ b/test/react/ClassComponents/complex-class-with-equivalent-node.js @@ -0,0 +1,51 @@ +const React = require("react"); + +function Tau(props) { + return React.createElement( + "div", + null, + React.createElement(Epsilon, { + a: props.z, + }), + React.createElement(Zeta, { + p: props.h, + }) + ); +} + +class Epsilon extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return React.createElement(Zeta, { p: this.props.a }); + } +} + +function Zeta(props) { + return props.p ? null : React.createElement("foobar", null); +} + +if (this.__optimizeReactComponentTree) __optimizeReactComponentTree(Tau); + +Tau.getTrials = function(renderer, Root) { + const trials = []; + + renderer.update(); + trials.push(["render 1", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 2", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 3", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 4", renderer.toJSON()]); + + return trials; +}; + +module.exports = Tau; diff --git a/test/react/__snapshots__/ClassComponents-test.js.snap b/test/react/__snapshots__/ClassComponents-test.js.snap index 40ba42a81..c0b3f5a99 100644 --- a/test/react/__snapshots__/ClassComponents-test.js.snap +++ b/test/react/__snapshots__/ClassComponents-test.js.snap @@ -68,6 +68,278 @@ ReactStatistics { } `; +exports[`Complex class component hoists nodes independently of functional root component: (JSX => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (JSX => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (createElement => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (createElement => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (JSX => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (JSX => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (createElement => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (createElement => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + exports[`Complex class components folding into functional root component #2: (JSX => JSX) 1`] = ` ReactStatistics { "componentsEvaluated": 4,