[patch] Fix for constant propagation bug in VR Store

--- 

Changes in `@enableOptimizeFunctionExpressions` caused a bug in the last Forget 
sync to VR Store. The repro can be summarized to something like this: 

```js 

function foo() { 

const x = true; // some constant or global 

// Add some branching for type inference 

// This can be a Logical expression as well (e.g. `4 || 5`) 

if (...) { } 

// In this HIR block, SSA inserts a `x$2 = phi(x$1, x$1)`. 

// EliminateRedundantPhiNodes needs to rewrite all references of `x$2` to `x$1` 

const accessXInLambda = () => x; 

return accessXInLambda; 

}
This commit is contained in:
Mofei Zhang
2023-08-02 17:30:57 -04:00
parent 50b0954279
commit 015029dc65
4 changed files with 189 additions and 3 deletions
@@ -27,9 +27,13 @@ import {
* and phis rewrite all their identifiers based on this table. The algorithm loops over the CFG repeatedly
* until there are no new rewrites: for a CFG without back-edges it completes in a single pass.
*/
export function eliminateRedundantPhi(fn: HIRFunction): void {
export function eliminateRedundantPhi(
fn: HIRFunction,
sharedRewrites?: Map<Identifier, Identifier>
): void {
const ir = fn.body;
const rewrites: Map<Identifier, Identifier> = new Map();
const rewrites: Map<Identifier, Identifier> =
sharedRewrites != null ? sharedRewrites : new Map();
// Whether or the CFG has a back-edge (a loop). We determine this dynamically
// during the first iteration over the CFG by recording which blocks were already
@@ -102,6 +106,13 @@ export function eliminateRedundantPhi(fn: HIRFunction): void {
for (const place of eachInstructionOperand(instr)) {
rewritePlace(place, rewrites);
}
if (instr.value.kind === "FunctionExpression") {
const { context } = instr.value.loweredFunc;
for (const place of context) {
rewritePlace(place, rewrites);
}
}
rewritePlace(instr.lvalue, rewrites);
// visit function expressions on first iteration of each block
@@ -110,7 +121,7 @@ export function eliminateRedundantPhi(fn: HIRFunction): void {
instr.value.kind === "FunctionExpression" &&
fn.env.enableOptimizeFunctionExpressions
) {
eliminateRedundantPhi(instr.value.loweredFunc);
eliminateRedundantPhi(instr.value.loweredFunc, rewrites);
}
}
@@ -0,0 +1,124 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as React from "react";
import { render } from "@testing-library/react";
globalThis.constantValue = "global test value";
test("literal-constant-propagation", () => {
function Component() {
const x = "test value 1";
return <div>{x}</div>;
}
const { asFragment, rerender } = render(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
rerender(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
});
test("global-constant-propagation", () => {
function Component() {
const x = constantValue;
return <div>{x}</div>;
}
const { asFragment, rerender } = render(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
global test value
</div>
</DocumentFragment>
`);
rerender(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
global test value
</div>
</DocumentFragment>
`);
});
test("lambda-constant-propagation", () => {
function Component() {
const x = "test value 1";
const getDiv = () => <div>{x}</div>;
return getDiv();
}
const { asFragment, rerender } = render(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
rerender(<Component />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
});
test("lambda-constant-propagation-of-phi-node", () => {
function Component({ noopCallback }) {
const x = "test value 1";
if (constantValue) {
noopCallback();
}
const getDiv = () => <div>{x}</div>;
return getDiv();
}
const { asFragment, rerender } = render(
<Component noopCallback={() => {}} />
);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
rerender(<Component noopCallback={() => {}} />);
expect(asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div>
test value 1
</div>
</DocumentFragment>
`);
});
@@ -0,0 +1,43 @@
## Input
```javascript
function ConstantPropagationBug() {
const x = CONSTANT1;
const createPhiNode = CONSTANT2 || 5;
const getFoo = () => <Foo x={x} y={createPhiNode} />;
return getFoo();
}
```
## Code
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function ConstantPropagationBug() {
const $ = useMemoCache(2);
const createPhiNode = CONSTANT2 || 5;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = () => <Foo x={CONSTANT1} y={createPhiNode} />;
$[0] = t0;
} else {
t0 = $[0];
}
const getFoo = t0;
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getFoo();
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
```
@@ -0,0 +1,8 @@
function ConstantPropagationBug() {
const x = CONSTANT1;
const createPhiNode = CONSTANT2 || 5;
const getFoo = () => <Foo x={x} y={createPhiNode} />;
return getFoo();
}